Bladeren bron

Merge branch 'master' of http://39.106.8.246:3003/doc/saga-doc

haojianlong 4 jaren geleden
bovenliggende
commit
9189b9ce54
49 gewijzigde bestanden met toevoegingen van 5632 en 19 verwijderingen
  1. 3 0
      docs/.vuepress/components/example/web/graph/scene/Undo1.vue
  2. 44 0
      docs/dev/data-center-scheme/data-center-platform.md
  3. 299 0
      docs/dev/data-center-scheme/dict-dataplatform.md
  4. BIN
      docs/dev/data-center-scheme/img/wenti001.png
  5. BIN
      docs/dev/data-center-scheme/img/wenti002.png
  6. BIN
      docs/dev/data-center-scheme/img/wenti003.png
  7. BIN
      docs/dev/data-center-scheme/img/wenti004.png
  8. 2 1
      docs/dev/data-center-scheme/index.js
  9. 5 1
      docs/dev/data-center/index.js
  10. 2 2
      docs/dev/data-center/relations/belongs/Sp2Sp.md
  11. 7 9
      docs/dev/data-center/relations/belongs/Sp2Sp2.md
  12. 575 0
      docs/dev/data-center/relations/belongs/Sp2Sp_convection.md
  13. 533 0
      docs/dev/data-center/relations/belongs/Sp2Sp_light.md
  14. 493 0
      docs/dev/data-center/relations/belongs/Sp2Sp_transportation.md
  15. 1 1
      docs/dev/data-center/relations/belongs/Sy2Sp.md
  16. 5 2
      docs/dev/data-center/relations/belongs/f_rel_calc.md
  17. BIN
      docs/dev/data-center/relations/belongs/img/sp2sp_light-1.png
  18. BIN
      docs/dev/data-center/relations/belongs/img/sp2sp_transportation-1.png
  19. BIN
      docs/dev/data-center/relations/problems/img/problem-1.png
  20. BIN
      docs/dev/data-center/relations/problems/img/problem-2.png
  21. BIN
      docs/dev/data-center/relations/problems/img/problem-3.png
  22. 3 0
      docs/dev/data-center/relations/problems/problems.md
  23. 19 0
      docs/dev/data-center/relations/problems/vwall_problem.md
  24. 468 2
      docs/dev/paas/data-platform.md
  25. 83 0
      docs/dev/saga-graphy/graphy-engine/style.md
  26. 639 0
      docs/setup/centos/app-server/MBI.md
  27. 139 0
      docs/setup/centos/app-server/centos-postgresql-plpython3.md
  28. BIN
      docs/setup/centos/app-server/img/mbi_001.png
  29. BIN
      docs/setup/centos/app-server/img/mbi_002.png
  30. BIN
      docs/setup/centos/app-server/img/mbi_003.png
  31. BIN
      docs/setup/centos/app-server/img/mbi_004.png
  32. BIN
      docs/setup/centos/app-server/img/plpython3_001.png
  33. BIN
      docs/setup/centos/app-server/img/revit_001.jpg
  34. BIN
      docs/setup/centos/app-server/img/revit_002.jpg
  35. BIN
      docs/setup/centos/app-server/img/revit_003.jpg
  36. BIN
      docs/setup/centos/app-server/img/scheduler_001.jpg
  37. 57 0
      docs/setup/centos/app-server/revit-algorithm.md
  38. 540 0
      docs/setup/centos/app-server/revit-check.md
  39. 876 0
      docs/setup/centos/app-server/revit-format.md
  40. 11 0
      docs/setup/centos/app-server/revit-service.md
  41. 36 0
      docs/setup/centos/app-server/scheduler.md
  42. BIN
      docs/setup/centos/server/img/pgdocker_001.jpg
  43. BIN
      docs/setup/centos/server/img/pgdocker_002.png
  44. BIN
      docs/setup/centos/server/img/pgdocker_003.jpg
  45. BIN
      docs/setup/centos/server/img/pgdocker_004.jpg
  46. 780 0
      docs/setup/centos/server/postgresql-docker.md
  47. 1 0
      docs/setup/centos/server/python.md
  48. 9 1
      docs/setup/index.js
  49. 2 0
      docs/setup/windows/server/python.md

+ 3 - 0
docs/.vuepress/components/example/web/graph/scene/Undo1.vue

@@ -25,6 +25,8 @@
             this.moveable = true;
             this.selectable = true;
             this.selected = true;
+            this.isTransform = true;
+            this.rolate = 60;
         }
 
         boundingRect(): SRect {
@@ -47,6 +49,7 @@
             super(parent);
             this.moveable = true;
             this.selectable = true;
+            this.isTransform = false;
         }
 
         boundingRect(): SRect {

+ 44 - 0
docs/dev/data-center-scheme/data-center-platform.md

@@ -0,0 +1,44 @@
+# 数据中心对接物理世界数据同步文档
+
+
+#### 数据中心同步物理世界
+
+1.在物理世界注册同步项目应用
+2.订阅所有项目下的对象类型,以及对象类型对应的所有信息点
+3.根据新版字段结构把数据字典缓存数据中心,重新设计表结构
+4.数据中心所有对象或关系的创建、修改、删除都通过缓存到数据库表存储
+5.通过读取缓存数据表获取需要同步的对象,或关系 同步到新物理世界
+6.数据中心和物理世界对象或关系数据类型重新做转换
+7.MQ机制,数据中心到同步数据服务,同步服务到新物理世界,反之新物理世界发送消息到同步服务,同步服务直接把数据处理到数据中心数据库
+8.通过消息机制处理新物理世界到数据中心的 对象或关系数据
+9.通过消息机制处理新版字典数据的订阅更新,对应处理数据
+10.注意收到字典更新消息时优先处理字典更新,处理完字典后再继续处理对象数据同步
+11.新物理世界消息机制还没有确定 RabbitMQ还是ActiveMQ 
+综上所述 整个同步服务需要重新开发一套出来
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 待解决疑问
+
+
+4.消息通讯?
+
+
+
+#### 数据同步 返回数据全集 创建  修改 不受订阅限制
+创建  修改  查询   
+
+消息通讯机制  哪个应用的创建 修改删除 

+ 299 - 0
docs/dev/data-center-scheme/dict-dataplatform.md

@@ -414,6 +414,274 @@ POST: usercenter/project/save
 }
 ```
 
+#### 查询 项目(project)建筑(building)、楼层(floor)下的信息点 
+
+```
+GET: dict/funcid?&groupId=WD&classCode=building
+```
+请求参数
+
+| 参数名    | 说明                                    |      |
+| --------- | --------------------------------------- | ---- |
+| classCode   | 类型编码                                | 必选 |
+| groupId   | 集团id                                  | 可选 |
+| schemeId  | 集团方案id                              | 可选 |
+| projectId | 项目id                                  | 可选 |
+| appId     | 应用id,传入appId时必须同时传入projectId | 可选 |
+|           |                                         |      |
+请求参数说明
+```
+1.传入projectId是必须同时传入groupId
+2.传入projectId时对应项目必须已配置集团方案,schemeId可以不传,否则报错"当前项目未配置集团方案"
+3.传入appId时必须同时传入projectId
+
+```
+返回参数
+
+```
+{
+    "result": "success",
+    "count": 159,
+    "data": funcid[] // 信息点列表
+}
+```
+
+
+### 查询系统的结构树 需要先查询出来专业 再查询每个专业下的系统
+#### 查询所有专业
+```
+POST: dict/major/query
+```
+
+请求参数
+
+```
+{
+	"criteria": {
+	}
+}
+```
+
+应答内容
+
+```
+{
+    "result":"success",
+    "data":[
+        {
+            "id":1,
+            "code":"SE",
+            "name":"强电专业",
+            "aliasCode":"SE",
+            "aliasName":"强电专业",
+            "type":"common",
+            "groupId":"0"
+        },
+        {
+            "id":2,
+            "code":"WE",
+            "name":"弱电专业",
+            "aliasCode":"WE",
+            "aliasName":"弱电专业",
+            "type":"common",
+            "groupId":"0"
+        }
+    ],
+    "count":2
+}
+```
+
+#### 查询专业下的系统
+
+```
+/dict/class/query
+```
+
+请求参数
+
+```
+{
+    "criteria": {
+      "majorCode":{"$in":["SE"]}
+    }
+}
+```
+
+应答内容
+
+```
+{
+    "result": "success",
+    "data": [
+        {
+            "code": "SEBPBA",
+            "name": "UPS",
+            "aliasCode": "SEBPBA",
+            "aliasName": "UPS",
+            "parentCode": "equipment",
+            "majorCode": "SE",
+            "systemCode": "SEBP"
+        },
+        {
+            "code": "SEBPBP",
+            "name": "备用电源",
+            "aliasCode": "SEBPBP",
+            "aliasName": "EPS",
+            "parentCode": "equipment",
+            "majorCode": "SE",
+            "systemCode": "SEBP"
+        }
+    ],
+    "count": 2
+}
+```
+
+
+
+说明:
+```
+majorCode           是指专业编码
+systemCode          系统编码
+
+```
+
+### 设备类型树结构
+#### 需要先获取专业再根据专业获取系统,然后根据系统获取系统下的设备 获取专业以及系统见上面,下面是直接获取系统下的设备类型
+```
+/dict/class/query
+```
+
+请求参数
+
+```
+{
+    "criteria": {
+      "systemCode":{"$in":["ACAT"]}
+    }
+}
+```
+
+应答内容
+
+```
+{
+    "result": "success",
+    "data": [
+        {
+            "code": "ACATAA",
+            "name": "吊顶风机",
+            "aliasCode": "ACATAA",
+            "aliasName": "吊顶风机",
+            "parentCode": "equipment",
+            "majorCode": "AC",
+            "systemCode": "ACAT"
+        },
+        {
+            "code": "ACATAH",
+            "name": "空调机组",
+            "aliasCode": "ACATAH",
+            "aliasName": "空调机组",
+            "parentCode": "equipment",
+            "majorCode": "AC",
+            "systemCode": "ACAT"
+        }
+    ],
+    "count": 2
+}
+```
+
+
+#### 获取部件类型树结构  需要先获取到所有的部件然后根据部件对应找到设备类型,拼接组成部件树结构
+```
+/dict/class/query
+```
+
+请求参数
+
+```
+{
+  "criteria": {
+    "parentCode": {
+      "$in": [
+        "component"
+      ]
+    }
+  }
+}
+```
+
+应答内容
+
+```
+{
+    "result": "success",
+    "data": [
+        {
+            "code": "SETDHSIP",
+            "name": "综合保护装置",
+            "aliasCode": "SETDHSIP",
+            "aliasName": "综合保护装置",
+            "parentCode": "component",
+            "majorCode": "SE",
+            "systemCode": "SETD",
+            "equipmentCode": "SETDHS"
+        },
+        {
+            "code": "SETDLSDW",
+            "name": "低压配电抽屉",
+            "aliasCode": "SETDLSDW",
+            "aliasName": "低压配电抽屉",
+            "parentCode": "component",
+            "majorCode": "SE",
+            "systemCode": "SETD",
+            "equipmentCode": "SETDLS"
+        }
+    ],
+    "count": 2
+}
+```
+#### 根据部件的 equipmentCode 查询设备类型 然后拼接成部件树结构
+
+```
+/dict/class/query
+```
+
+请求参数
+
+```
+{
+    "criteria": {
+      "code":{"$in":["SETDHS"]}
+    }
+}
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
 
 
 
@@ -424,7 +692,38 @@ POST: usercenter/project/save
 
 ## 注意事项
 1.某一个类型显示子分类功能新版字典还没有实现
+2.空间类型无法获取
+3.编码问题  例如  系统是两位编码  加上专业是四位编码 code 是为了适配别名?
+```
+ {
+            "code": "SEBPBP",
+            "name": "备用电源",
+            "aliasCode": "SEBPBP",
+            "aliasName": "EPS",
+            "parentCode": "equipment",
+            "majorCode": "SE",
+            "systemCode": "SEBP"
+ }
+```
+4. 创建子分类?
+5. 获取到系统树结构,怎么样知道系统哪些类型中的信息点处于待审核、审核中状态
+![问题](./img/wenti001.png)
+6.怎么使用信息点扩展分类
+![问题](./img/wenti002.png)
+7.修改类型的别名称以及所属的父类 系统和设备都需要
+![问题](./img/wenti003.png)
+8.添加系统类型或设备类型 复制信息点操作
+![问题](./img/wenti004.png)
+
+
+
+
+
+
+
+
 
+## 调换角度,直接使用新版数据字典数库的角度,评定新版数据字典需要实现的接口
 
 
 

BIN
docs/dev/data-center-scheme/img/wenti001.png


BIN
docs/dev/data-center-scheme/img/wenti002.png


BIN
docs/dev/data-center-scheme/img/wenti003.png


BIN
docs/dev/data-center-scheme/img/wenti004.png


+ 2 - 1
docs/dev/data-center-scheme/index.js

@@ -2,7 +2,8 @@ const content = [
 
             // ["/dev/data-center-scheme/dict-dataplatform", "中台工具字典接口使用对接方案"],
             ["/dev/data-center-scheme/dict-ADM", "新版字典ADM对接方案"],
-            ["/dev/data-center-scheme/dict-dataplatform", "关系表设计"]
+            ["/dev/data-center-scheme/data-center-platform", "数据中心对接物理世界"]
+
 ];
 
 module.exports = content;

+ 5 - 1
docs/dev/data-center/index.js

@@ -33,6 +33,9 @@ const content = [
             ["/dev/data-center/relations/belongs/Sp2Fl", "业务空间所属楼层"],
             ["/dev/data-center/relations/belongs/Sp2Sp", "业务空间邻接关系"],
             ["/dev/data-center/relations/belongs/Sp2Sp2", "业务空间邻接关系(新)"],
+            ["/dev/data-center/relations/belongs/Sp2Sp_convection", "业务空间空气流通关系"],
+            ["/dev/data-center/relations/belongs/Sp2Sp_transportation", "业务空间交通关系"],
+            ["/dev/data-center/relations/belongs/Sp2Sp_light", "业务空间关照辐射网络"],
             ["/dev/data-center/relations/belongs/Sy2Bd", "系统所在建筑"],
             ["/dev/data-center/relations/belongs/Sy2Fl", "系统所在楼层"],
             ["/dev/data-center/relations/belongs/Sy2Sh", "系统所在竖井"],
@@ -67,7 +70,8 @@ const content = [
         title: "现存问题",
         path: "/dev/data-center/relations/",
         children: [
-            ["/dev/data-center/relations/problems/problems", "现存问题"]
+            ["/dev/data-center/relations/problems/problems", "现存问题"],
+            ["/dev/data-center/relations/problems/vwall_problem", "虚拟墙问题"]
         ]
     },
     {

+ 2 - 2
docs/dev/data-center/relations/belongs/Sp2Sp.md

@@ -24,13 +24,13 @@ from relations.src.business_space_adjacent.adjacent import calc_space_adjacent
 try:
     # 将下面对数据库的操作作为一个事务, 出异常则自动rollback
     with plpy.subtransaction():
-        delete_plan = plpy.prepare("delete from r_spatial_connection where project_id = $1 and sign = 2", ["text"])
+        delete_plan = plpy.prepare("delete from r_spatial_connection where project_id = $1 and sign = 2 and graph_type = 'SpaceNeighborhood'", ["text"])
         delete_plan.execute([project_id])
         space_data_plan = plpy.prepare("SELECT sp.id, sp.project_id, rel.floor_id, sp.object_type, sp.bim_location, sp.outline FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id where sp.project_id = $1 and rel.project_id = $1 and outline is not null order by floor_id, object_type", ["text"])
         space_data = space_data_plan.execute([project_id])
         rel_data = calc_space_adjacent(space_data)
         for single_rel in rel_data:
-            delete_duplicate_plan = plpy.prepare("delete from r_spatial_connection where space_id_one = $1 and space_id_two = $2", ["text", "text"])
+            delete_duplicate_plan = plpy.prepare("delete from r_spatial_connection where space_id_one = $1 and space_id_two = $2 and graph_type = 'SpaceNeighborhood'", ["text", "text"])
             delete_duplicate_plan.execute([single_rel['space_id_one'], single_rel['space_id_two']])
             insert_plan = plpy.prepare("insert into r_spatial_connection(project_id, location_one, location_two, space_id_one, space_id_two, sign, graph_type, floor_id, zone_type) values($1, $2, $3, $4, $5, 2, 'SpaceNeighborhood', $6, $7)", ["text", "text", "text", "text", "text", "text", "text"])
             insert_plan.execute([project_id, single_rel['location_one'], single_rel['location_two'], single_rel['space_id_one'], single_rel['space_id_two'], single_rel['floor_id'], single_rel['zone_type']])

+ 7 - 9
docs/dev/data-center/relations/belongs/Sp2Sp2.md

@@ -46,10 +46,8 @@
 <summary>源码</summary>
 
 ```
-create function rel_sp2sp_v2(project_id character varying) returns boolean
-    language plpython3u
-as
-$$
+CREATE OR REPLACE FUNCTION "public"."rel_sp2sp_v2"("project_id" varchar)
+  RETURNS "pg_catalog"."bool" AS $BODY$
 import math
 import json
 from shapely.geometry import Polygon
@@ -300,14 +298,14 @@ with plpy.subtransaction():
     plpy.info("space data : {0}".format(len(space_data)))
     rel_data, space_info = calc_space_adjacent(space_data)
     # 删除以前的业务空间相邻关系
-    delete_plan = plpy.prepare("delete from r_spatial_connection where project_id = $1 and sign = 2", ["text"])
+    delete_plan = plpy.prepare("delete from r_spatial_connection where project_id = $1 and sign = 2 and graph_type = 'SpaceNeighborhood'", ["text"])
     delete_plan.execute([project_id])
     plpy.info("rel_data : {0}".format(len(rel_data)))
     for space_id1, to_space_set in rel_data.items():
         space1 = space_info[space_id1]
         for space_id2 in to_space_set:
             space2 = space_info[space_id2]
-            delete_duplicate_plan = plpy.prepare("delete from r_spatial_connection where space_id_one = $1 and space_id_two = $2", ["text", "text"])
+            delete_duplicate_plan = plpy.prepare("delete from r_spatial_connection where space_id_one = $1 and space_id_two = $2 and type = 'SpaceNeighborhood'", ["text", "text"])
             delete_duplicate_plan.execute([space_id1, space_id2])
             insert_plan = plpy.prepare("insert into r_spatial_connection(project_id, location_one, location_two, space_id_one, space_id_two, sign, graph_type, floor_id, zone_type) values($1, $2, $3, $4, $5, 2, 'SpaceNeighborhood', $6, $7)", ["text", "text", "text", "text", "text", "text", "text"])
             # plpy.info("{0}, {1}, {2}, {3}, {4}, {5}, {6}".format(project_id, space1.get(column_bim_location), space2.get(column_bim_location), space_id1, space2, space1.get(column_floor_id), space1.get(column_object_type)))
@@ -318,9 +316,9 @@ with plpy.subtransaction():
 #    return False
 #else:
 #    return True
-$$;
-
-alter function rel_sp2sp_v2(varchar) owner to postgres;
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
 
 
 

+ 575 - 0
docs/dev/data-center/relations/belongs/Sp2Sp_convection.md

@@ -0,0 +1,575 @@
+# 业务空间空气流通关系
+## 前置条件
+    1. 业务空间有所在楼层
+    2. 业务空间有外轮廓
+    3. 所在楼层有绑定的模型(即ModelId信息点有值)
+## 处理逻辑
+    1. 查出所有有所属楼层, 且所属楼层已绑定模型, 并且外轮廓不是null的业务空间
+    2. 根据所在楼层, 业务空间分区类型来将业务空间分为不同的组 (例如 : A 楼层下的默认业务空间是一组, A楼层下的空调分区是另外一组)
+    3. 计算每个分组内的业务空间的空气流通关系
+    计算空气流通关系算法 (如下图):
+        1. 处理逻辑1
+            1). 获取到该分组所在的楼层下所有的门和窗的轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 对于每个门, 在门的中心点上做垂直线段, 获得如图中的点A,B (线段AB长度暂定3m)
+            3). 然后找到A, B点分别所在的业务空间, 那么认为找到的业务空间有空气流通关系
+        2. 处理逻辑2
+            1). 获取该分组所在楼层下的所有元空间轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 如果一个元空间同时属于多个业务空间, 则认为这些业务空间有空气流通关系
+    4. 将上次计算的结果删除, 然后把新计算出的业务空间空气流通关系插入关系表中
+    
+    
+![image](./img/sp2sp_light-1.png)
+
+
+## 函数
+
+###  业务空间轮廓结构
+```
+[
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的外轮廓
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的第n个需要被排除的轮廓
+    ], // 子轮廓
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ]
+    ],
+]
+```
+
+<details>
+<summary>源码</summary>
+
+```
+create function rel_sp2sp_convection(project_id character varying) returns boolean
+as
+$BODY$
+
+import math
+import json
+from shapely.geometry import Polygon
+from matplotlib.path import Path
+
+column_project_id = 'project_id'
+column_location_one = 'location_one'
+column_location_two = 'location_two'
+column_space_id_one = 'space_id_one'
+column_space_id_two = 'space_id_two'
+column_zone_type = 'zone_type'
+column_added_polygon = 'polygon'
+
+column_id = 'id'
+column_bim_location = 'bim_location'
+column_floor_id = 'floor_id'
+column_object_type = 'object_type'
+column_outline = 'outline'
+key_x = 'X'
+key_y = 'Y'
+
+delta_distance = 200
+door_length = 1500
+
+
+# 获取两点之间的距离
+def get_segment_distance(x1, y1, x2, y2):
+    x_diff = x1 - x2
+    y_diff = y1 - y2
+    return math.sqrt(x_diff ** 2 + y_diff ** 2)
+
+
+# 获取垂直方向的斜率
+def get_vertical_k(k):
+    if k is None:
+        return 0
+    if k == 0:
+        return None
+    return 1 / k
+
+# 根据点 x, y 计算 y = kx + a 中的 a
+def get_a_by_point_k(x, y, k):
+    if k is None:
+        return None
+    return y - k * x
+
+
+# 在直线   y = kx + a 上找到距离点 base_x, base_y 距离为distance 的坐标点(两个坐标点)
+def get_point_by_distance(base_x, base_y, k, a, distance):
+    if k is None:
+        return base_x, base_y + distance, base_x, base_y - distance
+    vector_x1 = math.sqrt(distance ** 2 / (1 + k ** 2))
+    vector_x2 = -vector_x1
+    vector_y1 = k * vector_x1
+    vector_y2 = k * vector_x2
+    return base_x + vector_x1, base_y + vector_y1, base_x + vector_x2, base_y + vector_y2
+
+# 在直线 y = kx + a 上找到一个点, 该点距离点(base_x, base_y) 长度为 distance, 并且距离vec_x, vec_y最近
+def get_point_by_distance_on_segment(base_x, base_y, k, a, distance, vec_x, vec_y):
+    x1, y1, x2, y2 = get_point_by_distance(base_x, base_y, k, a, distance)
+    distance1 = get_segment_distance(x1, y1, vec_x, vec_y)
+    distance2 = get_segment_distance(x2, y2, vec_x, vec_y)
+    if distance1 > distance2:
+        return x2, y2
+    return x1, y1
+
+
+# 获取输入的两点之间开门之后门的坐标点
+# 返回格式
+# 如果线段距离小于等于door_length的情况 : x1, y1, x2, y2
+# 如果线段距离大于door_length的情况 (两端各开一个门): x1, y1, x2, y2, x3, y3, x4, y4
+def get_points(x1, y1, x2, y2):
+    # 如果两个点是同一个点
+    if x1 == y1 and x2 == y2:
+        return None, None, None, None
+    # 计算线段的距离
+    distance = get_segment_distance(x1, y1, x2, y2)
+    # 计算当前线段的 k
+    if x1 == x2:
+        k = None
+    else:
+        k = (y1 - y2) / (x1 - x2)
+    # 计算垂直方向的k
+    vertical_k = get_vertical_k(k)
+    # 计算当前线段的 a
+    a = get_a_by_point_k(x1, y1, k)
+    # 距离大于delta_distance, 则足够开两个门
+    if distance > delta_distance:
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, delta_distance, x2, y2)
+        seg_x2, seg_y2 = get_point_by_distance_on_segment(x2, y2, k, a, delta_distance, x1, y1)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        vertical_a2 = get_a_by_point_k(seg_x2, seg_y2, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        dest_x3, dest_y3, dest_x4, dest_y4 = get_point_by_distance(seg_x2, seg_y2, vertical_k, vertical_a2, door_length)
+        return [dest_x1, dest_x2, dest_x3, dest_x4], [dest_y1, dest_y2, dest_y3, dest_y4]
+    else:
+        # 距离太小, 在中间开门
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, distance/2, x2, y2)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        return [dest_x1, dest_x2], [dest_y1, dest_y2]
+
+# 获取Polygon对象
+def get_polygon(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Polygon(poly)
+
+# 获取Path对象
+def get_path(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Path(poly)
+
+# 在polygon1包含polygon2的时候, 检测是否polygon1内的空洞也包含polygon2
+def is_include(polygon1, poly2):
+    length1 = len(polygon1)
+    for i in range(1, length1):
+        poly1 = get_polygon(polygon1[i])
+        if poly1.overlaps(poly2):
+            return True
+        if poly1.equals(poly2) or poly1.contains(poly2):
+            return False
+    return True
+
+def is_sub_outline_overlap(polygon1, polygon2):
+    poly1 = get_polygon(polygon1[0])
+    poly2 = get_polygon(polygon2[0])
+    try:
+        if poly1.overlaps(poly2) or poly1.equals(poly2):
+            return True
+    except Exception as e:
+        return False
+    if poly1.contains(poly2) or poly1.equals(poly2):
+        return is_include(polygon1, poly2)
+    if poly2.contains(poly1) or poly2.equals(poly1):
+        return is_include(polygon2, poly1)
+    return False
+
+# 是否面积有重叠
+def is_overlap(polygon1, ispace_polygon):
+    length1 = len(polygon1)
+    length2 = len(ispace_polygon)
+    if length1 == 0 or length2 == 0:
+        return False
+
+    for i in range(length1):
+        for j in range(length2):
+            if is_sub_outline_overlap(polygon1[i], ispace_polygon):
+                return True
+    return False
+
+# 获取门的轮廓坐标
+def get_door_segment(door):
+    try:
+        # plpy.info(door.outline)
+        # door.outline = door.outline.replace('\'', '"')
+        # plpy.info("outline : {}".format(door.outline))
+        # door_outline = json.loads(door.outline)
+        # plpy.info("outline : {}".format(door_outline))
+
+        return door.outline[0][0].get(key_x), door.outline[0][0].get(key_y), door.outline[0][1].get(key_x), door.outline[0][1].get(key_y)
+    except Exception as e:
+        return 0, 0, 0, 0
+
+
+
+# 获取业务空间每个块的最外层轮廓, 组成arr返回, 如果数据不合法发生异常则返回None
+def get_outer_polygon_arr(raw_outline):
+    try:
+        arr = []
+        # outline_json = json.loads(raw_outline)
+        outline_json = raw_outline
+        for i in range(len(outline_json)):
+            try:
+                single_polygon = get_path(outline_json[i][0])
+            except Exception as ex:
+                # plpy.info(outline_json[i][0])
+                plpy.info('eroor getting polygon')
+                continue
+            arr.append(single_polygon)
+        return arr
+    except Exception as e:
+        plpy.warning("get outer polygon error")
+        return []
+
+
+def dic2obj(d):
+    """Convert Dict to Object.
+    """
+    top = type('new', (object,), d)
+    seqs = tuple, list, set, frozenset
+    for i, j in d.items():
+        j0 = j
+        try:
+            j0 = json.loads(j0)
+        except Exception as e:
+            pass
+        if isinstance(j0, dict):
+            setattr(top, i, dic2obj(j0))
+        elif isinstance(j0, seqs):
+            setattr(top, i, type(j0)(dic2obj(sj) if isinstance(sj, dict) else sj for sj in j0))
+        else:
+            setattr(top, i, j0)
+    return top
+# 将数据库对象转换成python对象
+def sqldata2objlist(data):
+    '''Convert Sqldata to Object list.
+    '''
+    return list(map(dic2obj,data))
+
+
+# 判断一个点是否在多边形数组中的某一个多边形内
+def is_point_in_polygons(point, polygon_arr):
+    try:
+        for polygon in polygon_arr:
+            try:
+                if polygon.contains_points([point], None, -0.0001):
+                    return True
+            except Exception as ee:
+                plpy.warning("point in polygon : {0}".format(ee))
+        return False
+    except Exception as e:
+        plpy.warning(e)
+        return False
+
+
+# 检查是否已经加过这个关系(单向检查)
+def check_is_in_rel(space_adjacent, probe_id, id):
+    if probe_id in space_adjacent:
+        rel_set = space_adjacent[probe_id]
+        if id in rel_set:
+            return True
+    return False
+
+# 检查是否关系存在(双向检查)
+def check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+    is_in = check_is_in_rel(space_adjacent, probe_id, id)
+    is_in = is_in or check_is_in_rel(space_adjacent, id, probe_id)
+    return is_in
+
+# 将业务空间相邻关系插入map中
+def insert_into_rel(space_adjacent, probe_id, id):
+    if check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+        return
+    if probe_id not in space_adjacent:
+        space_adjacent[probe_id] = set()
+    rel_set = space_adjacent[probe_id]
+    rel_set.add(id)
+
+
+# 将输入数据按照楼层id, 业务空间类型分类
+def classify(space_list):
+    current_floor_id = ''
+    current_object_type = ''
+    current_sub_arr = []
+    space_arr = []
+    for row in space_list:
+        if row.floor_id == current_floor_id and row.object_type == current_object_type:
+            current_sub_arr.append(row)
+        else:
+            current_floor_id = row.floor_id
+            current_object_type = row.object_type
+            current_sub_arr = [row]
+            space_arr.append(current_sub_arr)
+        row.path = get_outer_polygon_arr(row.outline)
+    for sub_arr in space_arr:
+        if len(sub_arr) == 1:
+            space_arr.remove(sub_arr)
+    return space_arr
+
+
+# 获取楼层上绑定的真正的模型id
+def get_real_model_id(models):
+    sql_str = ""
+    for model in models:
+        sql_str += '\'' + model.model_id + '\','
+    if sql_str.endswith(','):
+        sql_str = sql_str[0:-1]
+    real_model_id_plan = plpy.prepare("select mid.id, file.id fid, file.status from (select id, current_model_id from revit.model_floor where id in ({0})) mid left join revit.model_file file on mid.current_model_id = file.id".format(sql_str), [])
+    real_model_ids = real_model_id_plan.execute([])
+    floor_model_dict = dict()
+    for row in real_model_ids:
+        floor_id = row.get('id')
+        model_id = row.get('fid')
+        status = row.get('status')
+        if status == 4:
+            floor_model_dict[floor_id] = model_id
+    for model in models:
+        if model.model_id not in floor_model_dict:
+            models.remove(model)
+        else:
+            # plpy.info("mdoel id : {}, {}".format(model.model_id, floor_model_dict[model.model_id]))
+            model.model_id = floor_model_dict[model.model_id]
+
+# 获取门的数据
+def get_door_data(model_id):
+    door_sql = "select id, outline from graphtype.door where model_id = $1 and type = 'Door' and outline is not null and outline != '[]'"
+    door_plan = plpy.prepare(door_sql, ["text"])
+    door_data = door_plan.execute([model_id])
+    return sqldata2objlist(door_data)
+
+# 获取窗户数据
+def get_window_data(model_id):
+    window_sql = "select id, outline from graphtype.window where model_id = $1 and type = 'Window' and outline is not null and outline != '[]'"
+    window_plan = plpy.prepare(window_sql, ["text"])
+    window_data = window_plan.execute([model_id])
+    return sqldata2objlist(window_data)
+
+# 获取虚拟墙数据
+def get_virtual_wall_data(model_id):
+    vwall_sql = "select id, outline from graphtype.virtual_wall where model_id = $1 and type = 'VirtualWall' and outline is not null and outline != '[]'"
+    vwall_plan = plpy.prepare(vwall_sql, ["text"])
+    vwall_data = vwall_plan.execute([model_id])
+    return sqldata2objlist(vwall_data)
+
+
+# 获取元空间的数据
+def get_ispace_data(model_id):
+    # plpy.info("ispace model id : {}".format(model_id))
+    # ispace_sql = "select id, outline from zone_ispace where model_id = $1 and outline is not null"
+    ispace_sql = "select id, outline from revit.space where model_id = $1 and outline is not null"
+    ispace_plan = plpy.prepare(ispace_sql, ["text"])
+    ispace_data = ispace_plan.execute([model_id])
+    return sqldata2objlist(ispace_data)
+
+# 获取业务空间数据, 带所属楼层的信息
+def get_sp_floor_data(project_id):
+    #sql_str = "SELECT sp.id, rel.floor_id, sp.object_type, sp.bim_location, sp.outline FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id where sp.project_id = $1 and rel.project_id = $1 and outline is not null order by floor_id, object_type"
+    sql_str = "SELECT sp.id, rel.floor_id, fl.model_id, sp.object_type, sp.bim_location, sp.outline " \
+              "FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id left join floor fl on fl.id = rel.floor_id " \
+              "where sp.project_id = $1 and rel.project_id = $1 and fl.model_id is not null and sp.outline is not null order by floor_id, object_type"
+    space_data_plan = plpy.prepare(sql_str, ["text"])
+    space_data = space_data_plan.execute([project_id])
+    return sqldata2objlist(space_data)
+
+
+# 根据门的信息计算一个分组内的业务空间是否有关系
+def calc_space_adjacent_by_door(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        door_arr = get_door_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for door in door_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(door)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+                # 如果关系已经找完的话 退出循环
+                if len(rel_arr) == 2:
+                    break
+            if len(rel_arr) == 2:
+                insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+# 根据窗的信息计算一个分组内的业务空间是否有关系
+def calc_space_adjacent_by_window(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        window_arr = get_window_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for window in window_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(window)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+                # 如果关系已经找完的话 退出循环
+                if len(rel_arr) == 2:
+                    break
+            if len(rel_arr) == 2:
+                insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+# 根据窗的信息计算一个分组内的业务空间是否有关系
+def calc_space_adjacent_by_virtualwall(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        vwall_arr = get_virtual_wall_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for vwall in vwall_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(vwall)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+            for idx1 in range(len(rel_arr)):
+                for idx2 in range(idx1 + 1, len(rel_arr)):
+                    plpy.info("rel_arr : {}".format(rel_arr))
+                    insert_into_rel(space_adjacent, rel_arr[idx1], rel_arr[idx2])
+            # if len(rel_arr) == 2:
+            #     insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+
+# 根据元空间所属的多个业务空间计算一个分组内的业务空间是否有关系
+def calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        ispace_arr = get_ispace_data(space_row.model_id)
+        # plpy.info("ispace_arr : {}".format(len(ispace_arr)))
+        for ispace in ispace_arr:
+            # ispace_outline = json.loads(ispace.outline)
+            ispace_outline = ispace.outline
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                # space_outline = json.loads(probe_space_row.outline)
+                space_outline = probe_space_row.outline
+                space_info[probe_space_row.id] = probe_space_row
+                if is_overlap(space_outline, ispace_outline):
+                    rel_arr.append(probe_space_row.id)
+            for idx1 in range(len(rel_arr)):
+                for idx2 in range(idx1 + 1, len(rel_arr)):
+                    plpy.info("rel_arr : {}".format(rel_arr))
+                    insert_into_rel(space_adjacent, rel_arr[idx1], rel_arr[idx2])
+
+
+try:
+    # 将下面对数据库的操作作为一个事务, 出异常则自动rollback
+    with plpy.subtransaction():
+        space_adjacent = dict()
+        space_info = dict()
+        # 获取有所在楼层的所有业务空间, 并按照所在楼层和业务空间类型排序, 方便分组
+        space_data = get_sp_floor_data(project_id)
+        get_real_model_id(space_data)
+        # 给业务空间按照所属楼层和业务空间类型分组
+        space_arr = classify(space_data)
+        # plpy.info("space data : {0}, data : {1}".format(len(space_data), space_data))
+        calc_space_adjacent_by_door(space_arr, space_adjacent, space_info)
+        calc_space_adjacent_by_window(space_arr, space_adjacent, space_info)
+        #calc_space_adjacent_by_virtualwall(space_arr, space_adjacent, space_info)
+        calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info)
+        # 删除以前的业务空间相邻关系
+        delete_plan = plpy.prepare("delete from relationship.r_sp2sp where project_id = $1 and sign = 2 and type = 'sp2sp_ConvectionNetwork_1'", ["text"])
+        delete_plan.execute([project_id])
+        count = 0
+        for space_id1, to_space_set in space_adjacent.items():
+            space1 = space_info[space_id1]
+            for space_id2 in to_space_set:
+                # plpy.info("transport relation : {0} , {1}".format(space_id1, space_id2))
+                count = count + 1
+                space2 = space_info[space_id2]
+                delete_duplicate_plan = plpy.prepare("delete from relationship.r_sp2sp where id1 = $1 and id2 = $2", ["text", "text"])
+                delete_duplicate_plan.execute([space_id1, space_id2])
+                insert_plan = plpy.prepare("insert into relationship.r_sp2sp(project_id, id1, id2, type, sign, zone_type) values($1, $2, $3, 'sp2sp_ConvectionNetwork_1', 2, $4)",
+                                           ["text", "text", "text", "text"])
+                insert_plan.execute([project_id, space_id1, space_id2, space1.object_type])
+        plpy.info("sp2sp_light rel : {}".format(count))
+        return True
+except Exception as e:
+   plpy.warning(e)
+   return False
+else:
+   return True
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+
+
+
+select public.rel_sp2sp_convection('Pj1101050001')
+```
+
+</details>
+
+## 入参
+    1. 项目id
+## 返回结果
+    1. True 计算成功 | False 计算失败
+## 例子
+    select public.rel_sp2sp_convection('Pj1102290002');

+ 533 - 0
docs/dev/data-center/relations/belongs/Sp2Sp_light.md

@@ -0,0 +1,533 @@
+# 业务空间光照辐射网络关系
+## 前置条件
+    1. 业务空间有所在楼层
+    2. 业务空间有外轮廓
+    3. 所在楼层有绑定的模型(即ModelId信息点有值)
+## 处理逻辑
+    1. 查出所有有所属楼层, 且所属楼层已绑定模型, 并且外轮廓不是null的业务空间
+    2. 根据所在楼层, 业务空间分区类型来将业务空间分为不同的组 (例如 : A 楼层下的默认业务空间是一组, A楼层下的空调分区是另外一组)
+    3. 计算每个分组内的业务空间的光照辐射关系
+    计算光照辐射关系算法 (如下图):
+        1. 处理逻辑1
+            1). 获取到该分组所在的楼层下所有的门和窗的轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 对于每个门, 在门的中心点上做垂直线段, 获得如图中的点A,B (线段AB长度暂定3m)
+            3). 然后找到A, B点分别所在的业务空间, 那么认为找到的业务空间有光照辐射网络关系
+        2. 处理逻辑2
+            1). 获取该分组所在楼层下的所有元空间轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 如果一个元空间同时属于多个业务空间, 则认为这些业务空间有光照辐射关系
+    4. 将上次计算的结果删除, 然后把新计算出的业务空间光照辐射关系插入关系表中
+    
+   
+![image](./img/sp2sp_light-1.png)
+
+
+## 函数
+
+###  业务空间轮廓结构
+```
+[
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的外轮廓
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的第n个需要被排除的轮廓
+    ], // 子轮廓
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ]
+    ],
+]
+```
+
+<details>
+<summary>源码</summary>
+
+```
+create function rel_sp2sp_light(project_id character varying) returns boolean
+as
+$BODY$
+
+import math
+import json
+from shapely.geometry import Polygon
+from matplotlib.path import Path
+
+column_project_id = 'project_id'
+column_location_one = 'location_one'
+column_location_two = 'location_two'
+column_space_id_one = 'space_id_one'
+column_space_id_two = 'space_id_two'
+column_zone_type = 'zone_type'
+column_added_polygon = 'polygon'
+
+column_id = 'id'
+column_bim_location = 'bim_location'
+column_floor_id = 'floor_id'
+column_object_type = 'object_type'
+column_outline = 'outline'
+key_x = 'X'
+key_y = 'Y'
+
+delta_distance = 200
+door_length = 1500
+
+
+# 获取两点之间的距离
+def get_segment_distance(x1, y1, x2, y2):
+    x_diff = x1 - x2
+    y_diff = y1 - y2
+    return math.sqrt(x_diff ** 2 + y_diff ** 2)
+
+
+# 获取垂直方向的斜率
+def get_vertical_k(k):
+    if k is None:
+        return 0
+    if k == 0:
+        return None
+    return 1 / k
+
+# 根据点 x, y 计算 y = kx + a 中的 a
+def get_a_by_point_k(x, y, k):
+    if k is None:
+        return None
+    return y - k * x
+
+
+# 在直线   y = kx + a 上找到距离点 base_x, base_y 距离为distance 的坐标点(两个坐标点)
+def get_point_by_distance(base_x, base_y, k, a, distance):
+    if k is None:
+        return base_x, base_y + distance, base_x, base_y - distance
+    vector_x1 = math.sqrt(distance ** 2 / (1 + k ** 2))
+    vector_x2 = -vector_x1
+    vector_y1 = k * vector_x1
+    vector_y2 = k * vector_x2
+    return base_x + vector_x1, base_y + vector_y1, base_x + vector_x2, base_y + vector_y2
+
+# 在直线 y = kx + a 上找到一个点, 该点距离点(base_x, base_y) 长度为 distance, 并且距离vec_x, vec_y最近
+def get_point_by_distance_on_segment(base_x, base_y, k, a, distance, vec_x, vec_y):
+    x1, y1, x2, y2 = get_point_by_distance(base_x, base_y, k, a, distance)
+    distance1 = get_segment_distance(x1, y1, vec_x, vec_y)
+    distance2 = get_segment_distance(x2, y2, vec_x, vec_y)
+    if distance1 > distance2:
+        return x2, y2
+    return x1, y1
+
+
+# 获取输入的两点之间开门之后门的坐标点
+# 返回格式
+# 如果线段距离小于等于door_length的情况 : x1, y1, x2, y2
+# 如果线段距离大于door_length的情况 (两端各开一个门): x1, y1, x2, y2, x3, y3, x4, y4
+def get_points(x1, y1, x2, y2):
+    # 如果两个点是同一个点
+    if x1 == y1 and x2 == y2:
+        return None, None, None, None
+    # 计算线段的距离
+    distance = get_segment_distance(x1, y1, x2, y2)
+    # 计算当前线段的 k
+    if x1 == x2:
+        k = None
+    else:
+        k = (y1 - y2) / (x1 - x2)
+    # 计算垂直方向的k
+    vertical_k = get_vertical_k(k)
+    # 计算当前线段的 a
+    a = get_a_by_point_k(x1, y1, k)
+    # 距离大于delta_distance, 则足够开两个门
+    if distance > delta_distance:
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, delta_distance, x2, y2)
+        seg_x2, seg_y2 = get_point_by_distance_on_segment(x2, y2, k, a, delta_distance, x1, y1)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        vertical_a2 = get_a_by_point_k(seg_x2, seg_y2, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        dest_x3, dest_y3, dest_x4, dest_y4 = get_point_by_distance(seg_x2, seg_y2, vertical_k, vertical_a2, door_length)
+        return [dest_x1, dest_x2, dest_x3, dest_x4], [dest_y1, dest_y2, dest_y3, dest_y4]
+    else:
+        # 距离太小, 在中间开门
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, distance/2, x2, y2)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        return [dest_x1, dest_x2], [dest_y1, dest_y2]
+
+# 获取Polygon对象
+def get_polygon(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Polygon(poly)
+
+# 获取Path对象
+def get_path(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Path(poly)
+
+# 在polygon1包含polygon2的时候, 检测是否polygon1内的空洞也包含polygon2
+def is_include(polygon1, poly2):
+    length1 = len(polygon1)
+    for i in range(1, length1):
+        poly1 = get_polygon(polygon1[i])
+        if poly1.overlaps(poly2):
+            return True
+        if poly1.equals(poly2) or poly1.contains(poly2):
+            return False
+    return True
+
+def is_sub_outline_overlap(polygon1, polygon2):
+    poly1 = get_polygon(polygon1[0])
+    poly2 = get_polygon(polygon2[0])
+    try:
+        if poly1.overlaps(poly2) or poly1.equals(poly2):
+            return True
+    except Exception as e:
+        return False
+    if poly1.contains(poly2) or poly1.equals(poly2):
+        return is_include(polygon1, poly2)
+    if poly2.contains(poly1) or poly2.equals(poly1):
+        return is_include(polygon2, poly1)
+    return False
+
+# 是否面积有重叠
+def is_overlap(polygon1, ispace_polygon):
+    length1 = len(polygon1)
+    length2 = len(ispace_polygon)
+    if length1 == 0 or length2 == 0:
+        return False
+
+    for i in range(length1):
+        for j in range(length2):
+            if is_sub_outline_overlap(polygon1[i], ispace_polygon):
+                return True
+    return False
+
+# 获取门的轮廓坐标
+def get_door_segment(door):
+    try:
+        # plpy.info(door.outline)
+        # door.outline = door.outline.replace('\'', '"')
+        # plpy.info("outline : {}".format(door.outline))
+        # door_outline = json.loads(door.outline)
+        # plpy.info("outline : {}".format(door_outline))
+
+        return door.outline[0][0].get(key_x), door.outline[0][0].get(key_y), door.outline[0][1].get(key_x), door.outline[0][1].get(key_y)
+    except Exception as e:
+        return 0, 0, 0, 0
+
+
+
+# 获取业务空间每个块的最外层轮廓, 组成arr返回, 如果数据不合法发生异常则返回None
+def get_outer_polygon_arr(raw_outline):
+    try:
+        arr = []
+        # outline_json = json.loads(raw_outline)
+        outline_json = raw_outline
+        for i in range(len(outline_json)):
+            try:
+                single_polygon = get_path(outline_json[i][0])
+            except Exception as ex:
+                # plpy.info(outline_json[i][0])
+                plpy.info('eroor getting polygon')
+                continue
+            arr.append(single_polygon)
+        return arr
+    except Exception as e:
+        plpy.warning("get outer polygon error")
+        return []
+
+
+def dic2obj(d):
+    """Convert Dict to Object.
+    """
+    top = type('new', (object,), d)
+    seqs = tuple, list, set, frozenset
+    for i, j in d.items():
+        j0 = j
+        try:
+            j0 = json.loads(j0)
+        except Exception as e:
+            pass
+        if isinstance(j0, dict):
+            setattr(top, i, dic2obj(j0))
+        elif isinstance(j0, seqs):
+            setattr(top, i, type(j0)(dic2obj(sj) if isinstance(sj, dict) else sj for sj in j0))
+        else:
+            setattr(top, i, j0)
+    return top
+# 将数据库对象转换成python对象
+def sqldata2objlist(data):
+    '''Convert Sqldata to Object list.
+    '''
+    return list(map(dic2obj,data))
+
+
+# 判断一个点是否在多边形数组中的某一个多边形内
+def is_point_in_polygons(point, polygon_arr):
+    try:
+        for polygon in polygon_arr:
+            try:
+                if polygon.contains_points([point], None, -0.0001):
+                    return True
+            except Exception as ee:
+                plpy.warning("point in polygon : {0}".format(ee))
+        return False
+    except Exception as e:
+        plpy.warning(e)
+        return False
+
+
+# 检查是否已经加过这个关系(单向检查)
+def check_is_in_rel(space_adjacent, probe_id, id):
+    if probe_id in space_adjacent:
+        rel_set = space_adjacent[probe_id]
+        if id in rel_set:
+            return True
+    return False
+
+# 检查是否关系存在(双向检查)
+def check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+    is_in = check_is_in_rel(space_adjacent, probe_id, id)
+    is_in = is_in or check_is_in_rel(space_adjacent, id, probe_id)
+    return is_in
+
+# 将业务空间相邻关系插入map中
+def insert_into_rel(space_adjacent, probe_id, id):
+    if check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+        return
+    if probe_id not in space_adjacent:
+        space_adjacent[probe_id] = set()
+    rel_set = space_adjacent[probe_id]
+    rel_set.add(id)
+
+
+# 将输入数据按照楼层id, 业务空间类型分类
+def classify(space_list):
+    current_floor_id = ''
+    current_object_type = ''
+    current_sub_arr = []
+    space_arr = []
+    for row in space_list:
+        if row.floor_id == current_floor_id and row.object_type == current_object_type:
+            current_sub_arr.append(row)
+        else:
+            current_floor_id = row.floor_id
+            current_object_type = row.object_type
+            current_sub_arr = [row]
+            space_arr.append(current_sub_arr)
+        row.path = get_outer_polygon_arr(row.outline)
+    for sub_arr in space_arr:
+        if len(sub_arr) == 1:
+            space_arr.remove(sub_arr)
+    return space_arr
+
+
+# 获取楼层上绑定的真正的模型id
+def get_real_model_id(models):
+    sql_str = ""
+    for model in models:
+        sql_str += '\'' + model.model_id + '\','
+    if sql_str.endswith(','):
+        sql_str = sql_str[0:-1]
+    real_model_id_plan = plpy.prepare("select mid.id, file.id fid, file.status from (select id, current_model_id from revit.model_floor where id in ({0})) mid left join revit.model_file file on mid.current_model_id = file.id".format(sql_str), [])
+    real_model_ids = real_model_id_plan.execute([])
+    floor_model_dict = dict()
+    for row in real_model_ids:
+        floor_id = row.get('id')
+        model_id = row.get('fid')
+        status = row.get('status')
+        if status == 4:
+            floor_model_dict[floor_id] = model_id
+    for model in models:
+        if model.model_id not in floor_model_dict:
+            models.remove(model)
+        else:
+            # plpy.info("mdoel id : {}, {}".format(model.model_id, floor_model_dict[model.model_id]))
+            model.model_id = floor_model_dict[model.model_id]
+
+# 获取门的数据
+def get_door_data(model_id):
+    door_sql = "select id, outline from graphtype.door where model_id = $1 and type = 'Door' and outline is not null"
+    door_plan = plpy.prepare(door_sql, ["text"])
+    door_data = door_plan.execute([model_id])
+    return sqldata2objlist(door_data)
+
+
+def get_window_data(model_id):
+    window_sql = "select id, outline from graphtype.window where model_id = $1 and type = 'Window' and outline is not null and outline != '[]'"
+    window_plan = plpy.prepare(window_sql, ["text"])
+    window_data = window_plan.execute([model_id])
+    return sqldata2objlist(window_data)
+
+
+# 获取元空间的数据
+def get_ispace_data(model_id):
+    # plpy.info("ispace model id : {}".format(model_id))
+    # ispace_sql = "select id, outline from zone_ispace where model_id = $1 and outline is not null"
+    ispace_sql = "select id, outline from revit.space where model_id = $1 and outline is not null"
+    ispace_plan = plpy.prepare(ispace_sql, ["text"])
+    ispace_data = ispace_plan.execute([model_id])
+    return sqldata2objlist(ispace_data)
+
+# 获取业务空间数据, 带所属楼层的信息
+def get_sp_floor_data(project_id):
+    #sql_str = "SELECT sp.id, rel.floor_id, sp.object_type, sp.bim_location, sp.outline FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id where sp.project_id = $1 and rel.project_id = $1 and outline is not null order by floor_id, object_type"
+    sql_str = "SELECT sp.id, rel.floor_id, fl.model_id, sp.object_type, sp.bim_location, sp.outline " \
+              "FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id left join floor fl on fl.id = rel.floor_id " \
+              "where sp.project_id = $1 and rel.project_id = $1 and fl.model_id is not null and sp.outline is not null order by floor_id, object_type"
+    space_data_plan = plpy.prepare(sql_str, ["text"])
+    space_data = space_data_plan.execute([project_id])
+    return sqldata2objlist(space_data)
+
+
+# 根据门的信息计算一个分组内的业务空间是否相邻
+def calc_space_adjacent_by_door(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        door_arr = get_door_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for door in door_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(door)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+                # 如果关系已经找完的话 退出循环
+                if len(rel_arr) == 2:
+                    break
+            if len(rel_arr) == 2:
+                insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+# 根据窗的信息计算一个分组内的业务空间是否相邻
+def calc_space_adjacent_by_window(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        window_arr = get_window_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for window in window_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(window)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+                # 如果关系已经找完的话 退出循环
+                if len(rel_arr) == 2:
+                    break
+            if len(rel_arr) == 2:
+                insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+
+def calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        ispace_arr = get_ispace_data(space_row.model_id)
+        # plpy.info("ispace_arr : {}".format(len(ispace_arr)))
+        for ispace in ispace_arr:
+            # ispace_outline = json.loads(ispace.outline)
+            ispace_outline = ispace.outline
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                # space_outline = json.loads(probe_space_row.outline)
+                space_outline = probe_space_row.outline
+                space_info[probe_space_row.id] = probe_space_row
+                if is_overlap(space_outline, ispace_outline):
+                    rel_arr.append(probe_space_row.id)
+            for idx1 in range(len(rel_arr)):
+                for idx2 in range(idx1 + 1, len(rel_arr)):
+                    plpy.info("rel_arr : {}".format(rel_arr))
+                    insert_into_rel(space_adjacent, rel_arr[idx1], rel_arr[idx2])
+
+
+try:
+    # 将下面对数据库的操作作为一个事务, 出异常则自动rollback
+    with plpy.subtransaction():
+        space_adjacent = dict()
+        space_info = dict()
+        # 获取有所在楼层的所有业务空间, 并按照所在楼层和业务空间类型排序, 方便分组
+        space_data = get_sp_floor_data(project_id)
+        get_real_model_id(space_data)
+        # 给业务空间按照所属楼层和业务空间类型分组
+        space_arr = classify(space_data)
+        # plpy.info("space data : {0}, data : {1}".format(len(space_data), space_data))
+        calc_space_adjacent_by_door(space_arr, space_adjacent, space_info)
+        calc_space_adjacent_by_window(space_arr, space_adjacent, space_info)
+        calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info)
+        # 删除以前的业务空间相邻关系
+        delete_plan = plpy.prepare("delete from relationship.r_sp2sp where project_id = $1 and sign = 2 and type = 'sp2sp_RadiationNetwork_1'", ["text"])
+        delete_plan.execute([project_id])
+        count = 0
+        for space_id1, to_space_set in space_adjacent.items():
+            space1 = space_info[space_id1]
+            for space_id2 in to_space_set:
+                # plpy.info("transport relation : {0} , {1}".format(space_id1, space_id2))
+                count = count + 1
+                space2 = space_info[space_id2]
+                delete_duplicate_plan = plpy.prepare("delete from relationship.r_sp2sp where id1 = $1 and id2 = $2 and type = 'sp2sp_RadiationNetwork_1'", ["text", "text"])
+                delete_duplicate_plan.execute([space_id1, space_id2])
+                insert_plan = plpy.prepare("insert into relationship.r_sp2sp(project_id, id1, id2, type, sign, zone_type) values($1, $2, $3, 'sp2sp_RadiationNetwork_1', 2, $4)",
+                                           ["text", "text", "text", "text"])
+                insert_plan.execute([project_id, space_id1, space_id2, space1.object_type])
+        plpy.info("sp2sp_light rel : {}".format(count))
+        return True
+except Exception as e:
+   plpy.warning(e)
+   return False
+else:
+   return True
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+
+
+
+select public.rel_sp2sp_light('Pj1101050001')
+```
+
+</details>
+
+## 入参
+    1. 项目id
+## 返回结果
+    1. True 计算成功 | False 计算失败
+## 例子
+    select public.rel_sp2sp_light('Pj1102290002');

+ 493 - 0
docs/dev/data-center/relations/belongs/Sp2Sp_transportation.md

@@ -0,0 +1,493 @@
+# 业务空间交通关系
+## 前置条件
+    1. 业务空间有所在楼层
+    2. 业务空间有外轮廓
+    3. 所在楼层有绑定的模型(即ModelId信息点有值)
+## 处理逻辑
+    1. 查出所有有所属楼层, 且所属楼层已绑定模型, 并且外轮廓不是null的业务空间
+    2. 根据所在楼层, 业务空间分区类型来将业务空间分为不同的组 (例如 : A 楼层下的默认业务空间是一组, A楼层下的空调分区是另外一组)
+    3. 计算每个分组内的业务空间的交通关系
+    计算交通关系算法 (如下图):
+        1. 处理逻辑1
+            1). 获取到该分组所在的楼层下所有的门的轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 对于每个门, 在门的中心点上做垂直线段, 获得如图中的点A,B (线段AB长度暂定3m)
+            3). 然后找到A, B点分别所在的业务空间, 则认为找到的业务空间有交通关系
+        2. 处理逻辑2
+            1). 获取该分组所在楼层下的所有元空间轮廓信息(根据楼层ModelId绑定的模型获取)
+            2). 如果一个元空间同时属于多个业务空间, 则认为这些业务空间有交通关系
+    4. 将上次计算的结果删除, 然后把新计算出的业务空间交通关系插入关系表中
+    
+   
+![image](./img/sp2sp_light-1.png)
+
+
+## 函数
+
+###  业务空间轮廓结构
+```
+[
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的外轮廓
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的第n个需要被排除的轮廓
+    ], // 子轮廓
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ]
+    ],
+]
+```
+
+<details>
+<summary>源码</summary>
+
+```
+create function rel_sp2sp_transport(project_id character varying) returns boolean
+as
+$BODY$
+
+import math
+import json
+from shapely.geometry import Polygon
+from matplotlib.path import Path
+
+column_project_id = 'project_id'
+column_location_one = 'location_one'
+column_location_two = 'location_two'
+column_space_id_one = 'space_id_one'
+column_space_id_two = 'space_id_two'
+column_zone_type = 'zone_type'
+column_added_polygon = 'polygon'
+
+column_id = 'id'
+column_bim_location = 'bim_location'
+column_floor_id = 'floor_id'
+column_object_type = 'object_type'
+column_outline = 'outline'
+key_x = 'X'
+key_y = 'Y'
+
+delta_distance = 200
+door_length = 1500
+
+
+# 获取两点之间的距离
+def get_segment_distance(x1, y1, x2, y2):
+    x_diff = x1 - x2
+    y_diff = y1 - y2
+    return math.sqrt(x_diff ** 2 + y_diff ** 2)
+
+
+# 获取垂直方向的斜率
+def get_vertical_k(k):
+    if k is None:
+        return 0
+    if k == 0:
+        return None
+    return 1 / k
+
+# 根据点 x, y 计算 y = kx + a 中的 a
+def get_a_by_point_k(x, y, k):
+    if k is None:
+        return None
+    return y - k * x
+
+
+# 在直线   y = kx + a 上找到距离点 base_x, base_y 距离为distance 的坐标点(两个坐标点)
+def get_point_by_distance(base_x, base_y, k, a, distance):
+    if k is None:
+        return base_x, base_y + distance, base_x, base_y - distance
+    vector_x1 = math.sqrt(distance ** 2 / (1 + k ** 2))
+    vector_x2 = -vector_x1
+    vector_y1 = k * vector_x1
+    vector_y2 = k * vector_x2
+    return base_x + vector_x1, base_y + vector_y1, base_x + vector_x2, base_y + vector_y2
+
+# 在直线 y = kx + a 上找到一个点, 该点距离点(base_x, base_y) 长度为 distance, 并且距离vec_x, vec_y最近
+def get_point_by_distance_on_segment(base_x, base_y, k, a, distance, vec_x, vec_y):
+    x1, y1, x2, y2 = get_point_by_distance(base_x, base_y, k, a, distance)
+    distance1 = get_segment_distance(x1, y1, vec_x, vec_y)
+    distance2 = get_segment_distance(x2, y2, vec_x, vec_y)
+    if distance1 > distance2:
+        return x2, y2
+    return x1, y1
+
+
+# 获取输入的两点之间开门之后门的坐标点
+# 返回格式
+# 如果线段距离小于等于door_length的情况 : x1, y1, x2, y2
+# 如果线段距离大于door_length的情况 (两端各开一个门): x1, y1, x2, y2, x3, y3, x4, y4
+def get_points(x1, y1, x2, y2):
+    # 如果两个点是同一个点
+    if x1 == y1 and x2 == y2:
+        return None, None, None, None
+    # 计算线段的距离
+    distance = get_segment_distance(x1, y1, x2, y2)
+    # 计算当前线段的 k
+    if x1 == x2:
+        k = None
+    else:
+        k = (y1 - y2) / (x1 - x2)
+    # 计算垂直方向的k
+    vertical_k = get_vertical_k(k)
+    # 计算当前线段的 a
+    a = get_a_by_point_k(x1, y1, k)
+    # 距离大于delta_distance, 则足够开两个门
+    if distance > delta_distance:
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, delta_distance, x2, y2)
+        seg_x2, seg_y2 = get_point_by_distance_on_segment(x2, y2, k, a, delta_distance, x1, y1)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        vertical_a2 = get_a_by_point_k(seg_x2, seg_y2, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        dest_x3, dest_y3, dest_x4, dest_y4 = get_point_by_distance(seg_x2, seg_y2, vertical_k, vertical_a2, door_length)
+        return [dest_x1, dest_x2, dest_x3, dest_x4], [dest_y1, dest_y2, dest_y3, dest_y4]
+    else:
+        # 距离太小, 在中间开门
+        seg_x1, seg_y1 = get_point_by_distance_on_segment(x1, y1, k, a, distance/2, x2, y2)
+        vertical_a1 = get_a_by_point_k(seg_x1, seg_y1, vertical_k)
+        dest_x1, dest_y1, dest_x2, dest_y2 = get_point_by_distance(seg_x1, seg_y1, vertical_k, vertical_a1, door_length)
+        return [dest_x1, dest_x2], [dest_y1, dest_y2]
+
+# 获取Polygon对象
+def get_polygon(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Polygon(poly)
+
+# 获取Path对象
+def get_path(single_poly):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair.get(key_x), pair.get(key_y)))
+        # plpy.info("polygon : {}, {}".format(pair.get(key_x), pair.get(key_y)))
+    return Path(poly)
+
+# 在polygon1包含polygon2的时候, 检测是否polygon1内的空洞也包含polygon2
+def is_include(polygon1, poly2):
+    length1 = len(polygon1)
+    for i in range(1, length1):
+        poly1 = get_polygon(polygon1[i])
+        if poly1.overlaps(poly2):
+            return True
+        if poly1.equals(poly2) or poly1.contains(poly2):
+            return False
+    return True
+
+def is_sub_outline_overlap(polygon1, polygon2):
+    poly1 = get_polygon(polygon1[0])
+    poly2 = get_polygon(polygon2[0])
+    try:
+        if poly1.overlaps(poly2) or poly1.equals(poly2):
+            return True
+    except Exception as e:
+        return False
+    if poly1.contains(poly2) or poly1.equals(poly2):
+        return is_include(polygon1, poly2)
+    if poly2.contains(poly1) or poly2.equals(poly1):
+        return is_include(polygon2, poly1)
+    return False
+
+# 是否面积有重叠
+def is_overlap(polygon1, ispace_polygon):
+    length1 = len(polygon1)
+    length2 = len(ispace_polygon)
+    if length1 == 0 or length2 == 0:
+        return False
+
+    for i in range(length1):
+        for j in range(length2):
+            if is_sub_outline_overlap(polygon1[i], ispace_polygon):
+                return True
+    return False
+
+# 获取门的轮廓坐标
+def get_door_segment(door):
+    try:
+        # plpy.info(door.outline)
+        # door.outline = door.outline.replace('\'', '"')
+        # plpy.info("outline : {}".format(door.outline))
+        # door_outline = json.loads(door.outline)
+        # plpy.info("outline : {}".format(door_outline))
+
+        return door.outline[0][0].get(key_x), door.outline[0][0].get(key_y), door.outline[0][1].get(key_x), door.outline[0][1].get(key_y)
+    except Exception as e:
+        return 0, 0, 0, 0
+
+
+
+# 获取业务空间每个块的最外层轮廓, 组成arr返回, 如果数据不合法发生异常则返回None
+def get_outer_polygon_arr(raw_outline):
+    try:
+        arr = []
+        # outline_json = json.loads(raw_outline)
+        outline_json = raw_outline
+        for i in range(len(outline_json)):
+            try:
+                single_polygon = get_path(outline_json[i][0])
+            except Exception as ex:
+                # plpy.info(outline_json[i][0])
+                plpy.info('eroor getting polygon')
+                continue
+            arr.append(single_polygon)
+        return arr
+    except Exception as e:
+        plpy.warning("get outer polygon error")
+        return []
+
+
+def dic2obj(d):
+    """Convert Dict to Object.
+    """
+    top = type('new', (object,), d)
+    seqs = tuple, list, set, frozenset
+    for i, j in d.items():
+        j0 = j
+        try:
+            j0 = json.loads(j0)
+        except Exception as e:
+            pass
+        if isinstance(j0, dict):
+            setattr(top, i, dic2obj(j0))
+        elif isinstance(j0, seqs):
+            setattr(top, i, type(j0)(dic2obj(sj) if isinstance(sj, dict) else sj for sj in j0))
+        else:
+            setattr(top, i, j0)
+    return top
+# 将数据库对象转换成python对象
+def sqldata2objlist(data):
+    '''Convert Sqldata to Object list.
+    '''
+    return list(map(dic2obj,data))
+
+
+# 判断一个点是否在多边形数组中的某一个多边形内
+def is_point_in_polygons(point, polygon_arr):
+    try:
+        for polygon in polygon_arr:
+            try:
+                if polygon.contains_points([point], None, -0.0001):
+                    return True
+            except Exception as ee:
+                plpy.warning("point in polygon : {0}".format(ee))
+        return False
+    except Exception as e:
+        plpy.warning(e)
+        return False
+
+
+# 检查是否已经加过这个关系(单向检查)
+def check_is_in_rel(space_adjacent, probe_id, id):
+    if probe_id in space_adjacent:
+        rel_set = space_adjacent[probe_id]
+        if id in rel_set:
+            return True
+    return False
+
+# 检查是否关系存在(双向检查)
+def check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+    is_in = check_is_in_rel(space_adjacent, probe_id, id)
+    is_in = is_in or check_is_in_rel(space_adjacent, id, probe_id)
+    return is_in
+
+# 将业务空间相邻关系插入map中
+def insert_into_rel(space_adjacent, probe_id, id):
+    if check_is_in_rel_bidirection(space_adjacent, probe_id, id):
+        return
+    if probe_id not in space_adjacent:
+        space_adjacent[probe_id] = set()
+    rel_set = space_adjacent[probe_id]
+    rel_set.add(id)
+
+
+# 将输入数据按照楼层id, 业务空间类型分类
+def classify(space_list):
+    current_floor_id = ''
+    current_object_type = ''
+    current_sub_arr = []
+    space_arr = []
+    for row in space_list:
+        if row.floor_id == current_floor_id and row.object_type == current_object_type:
+            current_sub_arr.append(row)
+        else:
+            current_floor_id = row.floor_id
+            current_object_type = row.object_type
+            current_sub_arr = [row]
+            space_arr.append(current_sub_arr)
+        row.path = get_outer_polygon_arr(row.outline)
+    for sub_arr in space_arr:
+        if len(sub_arr) == 1:
+            space_arr.remove(sub_arr)
+    return space_arr
+
+
+# 获取楼层上绑定的真正的模型id
+def get_real_model_id(models):
+    sql_str = ""
+    for model in models:
+        sql_str += '\'' + model.model_id + '\','
+    if sql_str.endswith(','):
+        sql_str = sql_str[0:-1]
+    real_model_id_plan = plpy.prepare("select mid.id, file.id fid, file.status from (select id, current_model_id from revit.model_floor where id in ({0})) mid left join revit.model_file file on mid.current_model_id = file.id".format(sql_str), [])
+    real_model_ids = real_model_id_plan.execute([])
+    floor_model_dict = dict()
+    for row in real_model_ids:
+        floor_id = row.get('id')
+        model_id = row.get('fid')
+        status = row.get('status')
+        if status == 4:
+            floor_model_dict[floor_id] = model_id
+    for model in models:
+        if model.model_id not in floor_model_dict:
+            models.remove(model)
+        else:
+            # plpy.info("mdoel id : {}, {}".format(model.model_id, floor_model_dict[model.model_id]))
+            model.model_id = floor_model_dict[model.model_id]
+
+# 获取门的数据
+def get_door_data(model_id):
+    door_sql = "select id, outline from graphtype.door where model_id = $1 and type = 'Door'"
+    door_plan = plpy.prepare(door_sql, ["text"])
+    door_data = door_plan.execute([model_id])
+    return sqldata2objlist(door_data)
+
+# 获取元空间的数据
+def get_ispace_data(model_id):
+    # plpy.info("ispace model id : {}".format(model_id))
+    # ispace_sql = "select id, outline from zone_ispace where model_id = $1 and outline is not null"
+    ispace_sql = "select id, outline from revit.space where model_id = $1 and outline is not null"
+    ispace_plan = plpy.prepare(ispace_sql, ["text"])
+    ispace_data = ispace_plan.execute([model_id])
+    return sqldata2objlist(ispace_data)
+
+# 获取业务空间数据, 带所属楼层的信息
+def get_sp_floor_data(project_id):
+    #sql_str = "SELECT sp.id, rel.floor_id, sp.object_type, sp.bim_location, sp.outline FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id where sp.project_id = $1 and rel.project_id = $1 and outline is not null order by floor_id, object_type"
+    sql_str = "SELECT sp.id, rel.floor_id, fl.model_id, sp.object_type, sp.bim_location, sp.outline " \
+              "FROM zone_space_base sp inner join r_sp_in_fl rel on rel.space_id = sp.id left join floor fl on fl.id = rel.floor_id " \
+              "where sp.project_id = $1 and rel.project_id = $1 and fl.model_id is not null and sp.outline is not null order by floor_id, object_type"
+    space_data_plan = plpy.prepare(sql_str, ["text"])
+    space_data = space_data_plan.execute([project_id])
+    return sqldata2objlist(space_data)
+
+
+# 根据门的信息计算一个分组内的业务空间是否相邻
+def calc_space_adjacent_by_door(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        door_arr = get_door_data(space_row.model_id)
+        # plpy.info("door : {0}".format(len(door_arr)))
+        for door in door_arr:
+            dx1, dy1, dx2, dy2 = get_door_segment(door)
+            if dx1 == dx2 and dy1 == dy2:
+                continue
+            # plpy.info("door location : {}, {}, {}, {}".format(dx1, dy1, dx2, dy2))
+            x_arr, y_arr = get_points(dx1, dy1, dx2, dy2)
+            # plpy.info("x : {}, y : {}".format(x_arr, y_arr))
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                probe_polygon_arr = probe_space_row.path
+                space_info[probe_space_row.id] = probe_space_row
+                for arr_index in range(0, len(x_arr)):
+                    prob_x = x_arr[arr_index]
+                    prob_y = y_arr[arr_index]
+                    is_hit = is_point_in_polygons((prob_x, prob_y), probe_polygon_arr)
+                    if is_hit:
+                        # plpy.info("hit")
+                        rel_arr.append(probe_space_row.id)
+                        break
+                # 如果关系已经找完的话 退出循环
+                if len(rel_arr) == 2:
+                    break
+            if len(rel_arr) == 2:
+                insert_into_rel(space_adjacent, rel_arr[0], rel_arr[1])
+
+
+def calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info):
+    for space_sub_arr in space_arr:
+        # space_sub_arr 是一个楼层内的一类业务空间
+        space_row = space_sub_arr[0]
+        ispace_arr = get_ispace_data(space_row.model_id)
+        # plpy.info("ispace_arr : {}".format(len(ispace_arr)))
+        for ispace in ispace_arr:
+            # ispace_outline = json.loads(ispace.outline)
+            ispace_outline = ispace.outline
+            rel_arr = []
+            for probe_space_row in space_sub_arr:
+                # space_outline = json.loads(probe_space_row.outline)
+                space_outline = probe_space_row.outline
+                space_info[probe_space_row.id] = probe_space_row
+                if is_overlap(space_outline, ispace_outline):
+                    rel_arr.append(probe_space_row.id)
+            for idx1 in range(len(rel_arr)):
+                for idx2 in range(idx1 + 1, len(rel_arr)):
+                    plpy.info("rel_arr : {}".format(rel_arr))
+                    insert_into_rel(space_adjacent, rel_arr[idx1], rel_arr[idx2])
+
+
+try:
+    # 将下面对数据库的操作作为一个事务, 出异常则自动rollback
+    with plpy.subtransaction():
+        space_adjacent = dict()
+        space_info = dict()
+        # 获取有所在楼层的所有业务空间, 并按照所在楼层和业务空间类型排序, 方便分组
+        space_data = get_sp_floor_data(project_id)
+        get_real_model_id(space_data)
+        # 给业务空间按照所属楼层和业务空间类型分组
+        space_arr = classify(space_data)
+        # plpy.info("space data : {0}, data : {1}".format(len(space_data), space_data))
+        calc_space_adjacent_by_door(space_arr, space_adjacent, space_info)
+        calc_space_adjacent_by_ispace(space_arr, space_adjacent, space_info)
+        # 删除以前的业务空间相邻关系
+        delete_plan = plpy.prepare("delete from relationship.r_sp2sp where project_id = $1 and sign = 2 and type = 'sp2sp_TrafficNetwork_1'", ["text"])
+        delete_plan.execute([project_id])
+        count = 0
+        for space_id1, to_space_set in space_adjacent.items():
+            space1 = space_info[space_id1]
+            for space_id2 in to_space_set:
+                # plpy.info("transport relation : {0} , {1}".format(space_id1, space_id2))
+                count = count + 1
+                space2 = space_info[space_id2]
+                delete_duplicate_plan = plpy.prepare("delete from relationship.r_sp2sp where id1 = $1 and id2 = $2 and type = 'sp2sp_TrafficNetwork_1'", ["text", "text"])
+                delete_duplicate_plan.execute([space_id1, space_id2])
+                insert_plan = plpy.prepare("insert into relationship.r_sp2sp(project_id, id1, id2, type, sign, zone_type) values($1, $2, $3, 'sp2sp_TrafficNetwork_1', 2, $4)",
+                                           ["text", "text", "text", "text"])
+                insert_plan.execute([project_id, space_id1, space_id2, space1.object_type])
+        plpy.info("transport rel : {}".format(count))
+        return True
+except Exception as e:
+   plpy.warning(e)
+   return False
+else:
+   return True
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+
+
+
+
+select public.rel_sp2sp_transport('Pj1101050001')
+```
+
+</details>
+
+## 入参
+    1. 项目id
+## 返回结果
+    1. True 计算成功 | False 计算失败
+## 例子
+    select public.rel_sp2sp_transport('Pj1102290002');

+ 1 - 1
docs/dev/data-center/relations/belongs/Sy2Sp.md

@@ -1,4 +1,4 @@
-系统所在业务空间
+# 系统所在业务空间
 ## 前置条件
     1. 系统下有设备
     2. 系统下的设备有所在业务空间关系

+ 5 - 2
docs/dev/data-center/relations/belongs/f_rel_calc.md

@@ -15,11 +15,14 @@ CREATE OR REPLACE FUNCTION "public"."f_rel_calc"("func" text, "project_id" varch
   RETURNS "pg_catalog"."int4" AS $BODY$
 # 根据函数输入类型分类
 classify = {"eq2bd":1, "eq2fl":1, "sp2fl":1, "eq2sh":1, "pe2bd":1, "pe2fl":1, "pe2sh":1, "pe2sp":1, "sh2sh":1, "sy2bd":1,
-    "sy2fl":1, "sy2sh":1, "sy2sp":1, "sh2bd":1, "bd2sp":2, "eq2sp":3, "fl2fl":4}
+    "sy2fl":1, "sy2sh":1, "sy2sp":1, "sh2bd":1, "sp2sp_transport":1, "sp2sp_light":1, "sp2sp_convection":1, "bd2sp":2, "eq2sp":3, "fl2fl":4}
 dependency = {"eq2bd":["eq2fl"], "eq2sh":["eq2sp"], "eq2sp":["eq2fl", "sp2fl"], "pe2bd":["eq2bd"], "pe2fl":["eq2fl"], "pe2sh":["eq2sh"],
     "pe2sp":["eq2sp"], "sy2bd":["eq2bd"], "sy2fl":["eq2fl"], "sy2sh":["eq2sh"], "sy2sp":["eq2sp"]}
 def get_type_by_func_name(func_name):
-    return func_name.split('_')[-1]
+    if "_" in func_name:
+        return func_name[int(func_name.index("_")) + 1:]
+    else:
+        return func_name
 try:
     with plpy.subtransaction():
         type = get_type_by_func_name(func)

BIN
docs/dev/data-center/relations/belongs/img/sp2sp_light-1.png


BIN
docs/dev/data-center/relations/belongs/img/sp2sp_transportation-1.png


BIN
docs/dev/data-center/relations/problems/img/problem-1.png


BIN
docs/dev/data-center/relations/problems/img/problem-2.png


BIN
docs/dev/data-center/relations/problems/img/problem-3.png


+ 3 - 0
docs/dev/data-center/relations/problems/problems.md

@@ -26,6 +26,8 @@
 ### <font color=red>未解决</font>
     1. 管网计算分块逻辑有问题 (邢景灏)
         暂未详细查找原因
+    2. 因为暂时未找到虚拟墙的合适使用方式, 在计算业务空间之间的关系(部分)时, 只要使用到了虚拟墙, 目前忽略.(邢景灏)
+         
         
 ## 调度器历史和现存问题
 ### 已解决
@@ -39,6 +41,7 @@
     3. 由基于tcp/ip的CS架构切换为http接口调用方式, 服务器端的接口和结构调整(邢景灏)
     4. 客户端在切换处理对象(服务器端)时, 因为id格式和后台限制原因不能正常工作(邢景灏)
 
+### <font color=red>未解决</font>
 #### 架构问题
     1. 由基于tcp/ip的CS架构切换为http接口调用方式, 客户端的调整(庞利祥)
     2. 服务器端未与新客户端完成对接 (庞利祥, 邢景灏)

+ 19 - 0
docs/dev/data-center/relations/problems/vwall_problem.md

@@ -0,0 +1,19 @@
+### 虚拟墙在计算业务空间之间的关系的问题
+
+## 虚拟墙的应用
+
+    虚拟墙是人为根据实际的业务场景需求而建立的墙, 如图所示:
+    虚拟墙跟实体墙一样, 有分隔元空间的作用
+
+![image](./img/problem-1.png)
+
+### 问题1 -- 模型画的不规范
+   
+   1. 如图所示, 红色箭头锁指的四段虚拟墙实则是一段虚拟墙, 有两部分跟真实的墙重叠在了一起
+   2. 如果按照建模规范, 虚拟墙体不能跟真实墙体重叠
+
+![image](./img/problem-2.png)
+
+### 问题2 -- 目前不能使用 "开虚拟门" 的方式来处理虚拟墙
+![image](./img/problem-3.png)
+

+ 468 - 2
docs/dev/paas/data-platform.md

@@ -1,10 +1,8 @@
-# 物理世界接口文档
 ### 术语说明:
 
 #### 对象
 基于数据字典类型和信息点定于创建的实例
 
-
 ```
 数据字典默认每个类型都有id、name、localId、localName,需要项目和应用在数据字典服务中订阅
 id、name、localId、localName对应原来的(如设备)EquipID、EquipName、EquipLocalID、EquipLocalName,值也相同,修改一方会同时修改另一方(无论是否订阅另一方)
@@ -12,12 +10,20 @@ id、name、localId、localName对应原来的(如设备)EquipID、EquipName
 
 ```
 
+#### 关系
+```
+对象之间的关系,一般由4个字段组成:graphId, rel, from, to
+```
+
+
 ### 字段约定
 
 | 字段      | 说明                          | 备注 |
 | -------- | ----------------------------- | ---- |
 | objectId | 对象id                          |      |
 | classCode  | 类型编码                              |      |
+| graphId  | 关系数据字段,图实例id                              |      |
+| rel  | 关系数据字段,边类型                              |      |
 
 ### 数据结构
 
@@ -31,6 +37,17 @@ id、name、localId、localName对应原来的(如设备)EquipID、EquipName
 | 其他信息点编码   | 根据信息点定义   |                                                     |
 |            |          |                                                           |
 
+关系 relation
+
+| 字段       | 数据类型  | 说明                                                       |
+| ---------- | -------- | --------------------------------------------------------- |
+| graphId         | String   | 图实例id                                                 |
+| rel             | String   | 边类型                                             |
+| from            | String   | 对象id                                          |
+| to              | String   | 对象id                                              |
+| infos           | Object   | 关系附加信息                                              |
+| 
+
 ### 接口
 
 #### 通用参数
@@ -187,3 +204,452 @@ POST: /object/query
     "message": "错误信息,如果接口发生错误会返回该字段"
 }
 ```
+
+
+#### 基础接口: 查询图实例
+接口说明
+```
+目前图实例是内置数据,仅提供查询
+```
+请求地址
+```
+POST /graph/query
+```
+请求参数
+```
+{
+    "criteria": {
+        "graphType": "ArchSubset"
+    }
+}
+```
+返回内容
+```
+{
+  "result" : "success",
+  "data" : [ 
+      {
+        "id" : "GtArchSubset001",
+        "projectId" : "Pj3203020001",
+        "graphType" : "ArchSubset",
+        "sequenceId" : "001",
+        "valid" : true,
+        "createTime" : "20200519140942"
+      }
+  ]
+}
+
+```
+
+
+
+#### 基础接口: 创建关系
+接口说明
+```
+该接口是批量创建接口
+调用时需确保每一项的数据正确,如果有任意一个或多于一个元素存在错误则返回报错,同时参数正确的数据也不会保存
+同样的参数可以调用多次,如果参数对应的关系已存在则覆盖更新
+需要确保使用的graphId和rel是最新版字典定义的数据,接口会对graphId和rel进行校验(目前未校验)
+```
+请求地址
+```
+POST: /relation/save
+```
+请求参数
+```
+[ 
+    {
+      "graphId" : "ddd",
+      "rel" : "atob",
+      "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91",
+      "to" : "Si3203020001e86cab26efdc11e9872a55fddb5ea8bb"
+    }, {
+      "graphId" : "ddd",
+      "rel" : "atob",
+      "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91",
+      "to" : "Si32030200016f0b820a1a8c11eaac5ab725fa2ffefc"
+    } 
+]
+```
+返回内容
+```
+{
+  "result" : "fail",
+  "message" : "参数错误",
+  "data" : [ 
+      {
+        "result" : "success",
+        "target" : {
+          "graphId" : "ddd",
+          "rel" : "atob",
+          "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91",
+          "to" : "Si3203020001e86cab26efdc11e9872a55fddb5ea8bb"
+        }
+      }, {
+        "result" : "error",
+        "target" : {
+          "graphId" : "ddd",
+          "rel" : "atob",
+          "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91",
+          "to" : "abc"
+        },
+        "error" : "to对象Id[abc]不存在;"
+      } 
+  ]
+}
+```
+
+#### 基础接口: 删除关系
+接口说明
+```
+该接口是批量删除接口
+调用时需确保每一项的数据正确,如果有任意一个或多于一个元素存在错误则返回报错,同时参数正确的数据也不会处理
+根据入参删除一个或多个关系实例
+请务必明确入参逻辑
+```
+请求地址
+```
+POST /relation/delete
+```
+请求参数
+```
+[ 
+    {
+      "graphId" : "ddd",
+      "rel" : "atob",
+      "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91"
+    }, 
+    {
+      "graphId" : "ddd",
+      "rel" : "atob",
+      "to" : "Si32030200016f0b820a1a8c11eaac5ab725fa2ffefc"
+    } 
+]
+
+```
+返回内容
+```
+{
+  "result" : "success",
+  "data" : [ 
+      {
+        "result" : "success",
+        "target" : {
+          "graphId" : "ddd",
+          "rel" : "atob",
+          "from" : "Sp320302000147eab664592c4a818bc5aebf8ad88f91"
+        },
+        "deletedCount" : 2
+      }, {
+        "result" : "success",
+        "target" : {
+          "graphId" : "ddd",
+          "rel" : "atob",
+          "to" : "Si32030200016f0b820a1a8c11eaac5ab725fa2ffefc"
+        },
+        "deletedCount" : 0
+      } 
+  ],
+  "totalDeletedCount" : 2
+}
+```
+返回内容说明
+
+| 字段名       | 数据类型 | 说明  |
+| ----------  | -------- | ------------------------- |
+| data.deletedCount   | Integer   | 单个参数对应删除的关系的数量 |
+| totalDeletedCount       | Integer   | 所有参数对应删除的关系的数量 |
+|             |          |                           |
+
+
+#### 基础接口: 查询关系
+
+接口说明
+
+```
+不支持分页!
+from和to必须且只能传一个
+withColumns:infos, infos是关系附件的数据, 如果不传则不返回infos
+```
+
+请求地址
+
+```
+POST: /relation/query
+```
+
+请求参数
+
+```
+{
+    "criteria": {
+        "graphId": "GtElementSpNeighborhood001",
+        "rel": "10",
+        "from": "Eq3203020001e945bebd9b1447b9859638810e7d6147",
+        "to": {
+            "$in":[
+                "Sp320302000151a095a44c1d4fd7930d2f87d38cdf77",
+                "Sp32030200010b1d106c4a6d45698c781bf7356df8ba 
+            ]
+        }
+    },
+    "withColumns": ["infos"]
+}
+```
+返回内容
+
+```
+{
+    "result": "success",
+    "data": [
+        {
+            "graphId": "GtElementSpNeighborhood001",
+            "rel": "10",
+            "from": "Eq3203020001e945bebd9b1447b9859638810e7d6147",
+            "to": "Sp320302000151a095a44c1d4fd7930d2f87d38cdf77"
+        },
+        {
+            "graphId": "GtElementSpNeighborhood001",
+            "rel": "10",
+            "from": "Eq3203020001e945bebd9b1447b9859638810e7d6147",
+            "to": "Sp32030200010b1d106c4a6d45698c781bf7356df8ba"
+        }
+    ]
+    "message": "错误信息,如果接口发生错误会返回该字段"
+}
+```
+
+#### 基础接口: 查询关系对象
+
+接口说明
+
+```
+根据关系查询对象
+不支持分页!
+from和to必须且只能传一个, 
+如果传from同时可以传toClassCode, 如果传to同时可以传fromClassCode
+如果不传withColumns则查询到的对象默认只返回id, name, localId, localName, classCode
+```
+
+请求地址
+
+```
+POST: /relation/object/query
+```
+
+请求参数
+
+```
+{
+    "criteria": {
+        "graphId": "GtElementSpNeighborhood001",
+        "rel": "10",
+        "from": "Eq3203020001e945bebd9b1447b9859638810e7d6147",
+        "fromClassCode": "ACCCOT",
+        "to": {
+            "$in":[
+                "Sp320302000151a095a44c1d4fd7930d2f87d38cdf77",
+                "Sp32030200010b1d106c4a6d45698c781bf7356df8ba 
+            ]
+        },
+        "toClassCode": "space",
+    },
+    "withColumns": [
+        "id", "name", "localId", "localName", "classCode"
+    ]
+}
+```
+返回内容
+
+```
+{
+    "result": "success",
+    "data": [
+        {
+            "id": "Sp320302000151a095a44c1d4fd7930d2f87d38cdf77",
+            "name": "对象名称",
+            "localId": "对象本地编码",
+            "localName": "对象本地名称",
+            "classCode": "space"
+        }
+    ]
+    "message": "错误信息,如果接口发生错误会返回该字段"
+}
+```
+
+
+
+#### 基础接口: 查询实时数据
+
+接口说明
+```
+查询对象绑点信息点的实时数据
+```
+
+请求地址
+```
+POST: /object/data/current
+```
+
+请求参数
+```
+[
+    { "objectId": "Eqxxxxx", "infoCode": "Tdb" },
+    { "objectId": "Eqxxxxx", "infoCode": "CO2" },
+    { "objectId": "Eqxxxxx", "infoCode": "Hcho" }
+]
+```
+返回内容
+
+```
+{
+    "result": "success",
+    "data": [
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "Tdb", 
+            "infoVaue": "8000-123", 
+            "data": 25.6,
+            "time": "20200512172435"
+        },
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "Tdb", 
+            "infoVaue": "8000-123", 
+            "data": 25.6,
+            "time": "20200512172435"
+        },
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "Tdb", 
+            "error": "错误信息:信息点不存在,表号功能号配置错误,断数等"
+        }
+    ]
+}
+```
+返回内容说明
+```
+    infoValue: 信息点配置的内容即绑定的表号功能号
+    time: 实时数据的采集时间
+    data: 实时数据的值
+
+```
+
+#### 基础接口: 查询历史数据/分精度数据
+
+接口说明
+```
+查询对象绑点信息点的历史数据
+```
+
+请求地址
+```
+POST: /object/data/history
+```
+
+请求参数
+```
+{
+    "criteria": {
+        "objectId": "",
+        "infoCode": "",
+        "period": 300,
+        "time": {
+            "$gte": "20200415120000",
+            "$lt": "20200501120000"        
+        },
+        "asc": true
+    }
+}
+```
+参数说明
+```
+    objectId: 必填,对象id
+    infoCode: 必填,信息点ID
+    period: 必填,间隔, 单位为秒, 如300表示5分钟
+    time: 必填,历史数据的开始结束时间
+    asc: 对结果按时间排序, true表示升序, false表示降序, 默认为false
+        
+```
+返回内容
+
+```
+{
+    "result": "success",
+    "data": [
+        { 
+            "time": "20200415120000"
+            "data": 25.6,
+        },
+        { 
+            "time": "20200415120500"
+            "data": 25.6,
+        },
+        { 
+            "time": "20200415121000"
+            "data": 25.6,
+        }
+    ]
+}
+```
+返回内容说明
+```
+返回数据的time可能是不连续的
+
+```
+
+#### 基础接口: 发送控制指令/设定动态参数
+请求地址
+```
+POST: /object/funcid/setting
+```
+
+请求参数
+```
+[
+    { "objectId": "Eqxxxxx", "infoCode": "FanFreqSet", value: 24 },
+    { "objectId": "Eqxxxxx", "infoCode": "InValveSwitchSet", value: "a" },
+    { "objectId": "Eqxxxxx", "infoCode": "WaterInOutDetaTempSet", value: 10 }
+]
+```
+请求参数说明
+```
+value要严格匹配信息点定义的数据类型
+```
+返回内容
+
+```
+{
+    "result": "success",
+    "data": [
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "FanFreqSet", 
+            "value": 24,
+            "status": "sended"
+        },
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "InValveSwitchSet", 
+            "value": "a",
+            "status": "skip",
+            "error": "值错误"
+        },
+        { 
+            "objectId": "Eqxxxxx", 
+            "infoCode": "InValveSwitchSet", 
+            "value": "a",
+            "status": "send-timeout",
+            "error": "异常信息"
+        }
+    ]
+}
+```
+返回内容说明
+```
+    status: 指令发送状态
+    skip: 信息点未配置或值错误等会取消发送,
+    sended: 指令发送成功, 注意:指令发送和设备执行是异步的,指令发送成功并不表示设备会马上执行该指令,甚至不能确定设备将来会执行该指令
+    send-timeout: 连接采集程序超时
+    status的状态码后续会修订,文档中的值并不是最终版本
+```

+ 83 - 0
docs/dev/saga-graphy/graphy-engine/style.md

@@ -4,6 +4,89 @@
 [[toc]]
 :::
 
+## 颜色
+### 内置颜色
+SColor 内置了一些常用颜色。见下表:
+
+<table border="0">
+  <tr style="background:#00000000">
+    <td>SColor.Transparent</td>
+    <td>#00000000</td>
+  </tr>
+  <tr style="background:#000000;color:white">
+    <td>SColor.Black</td>
+    <td>#00000000</td>
+  </tr>
+  <tr style="background:#000080;color:white">
+    <td>SColor.DarkBlue</td>
+    <td>#000080</td>
+  </tr>
+  <tr style="background:#0000FF;color:white">
+    <td>SColor.Blue</td>
+    <td>#0000FF</td>
+  </tr>
+  <tr style="background:#008000;color:white">
+    <td>SColor.DarkGreen</td>
+    <td>#008000</td>
+  </tr>
+  <tr style="background:#00FF00;color:white">
+    <td>SColor.Green</td>
+    <td>#00FF00</td>
+  </tr>
+  <tr style="background:#008080;color:white">
+    <td>SColor.DarkCyan</td>
+    <td>#008080</td>
+  </tr>
+  <tr style="background:#00FFFF">
+    <td>SColor.Cyan</td>
+    <td>#00FFFF</td>
+  </tr>
+  <tr style="background:#800000;color:white">
+    <td>SColor.DarkRed</td>
+    <td>#800000</td>
+  </tr>
+  <tr style="background:#FF0000;color:white">
+    <td>SColor.Red</td>
+    <td>#FF0000</td>
+  </tr>
+  <tr style="background:#800080;color:white">
+    <td>SColor.DarkMagenta</td>
+    <td>#800080</td>
+  </tr>
+  <tr style="background:#FF00FF;color:white">
+    <td>SColor.Magenta</td>
+    <td>#FF00FF</td>
+  </tr>
+  <tr style="background:#808000;color:white">
+    <td>SColor.DarkYellow</td>
+    <td>#808000</td>
+  </tr>
+  <tr style="background:#FFFF00">
+    <td>SColor.Yellow</td>
+    <td>#00000000</td>
+  </tr>
+  <tr style="background:#FFFFFF">
+    <td>SColor.White</td>
+    <td>#FFFFFF</td>
+  </tr>
+  <tr style="background:#808080;color:white">
+    <td>SColor.DarkGray</td>
+    <td>#808080</td>
+  </tr>
+  <tr style="background:#A0A0A0">
+    <td>SColor.Gray</td>
+    <td>#A0A0A0</td>
+  </tr>   
+  <tr style="background:#C0C0C0">
+    <td>SColor.LightGray</td>
+    <td>#C0C0C0</td>
+  </tr>  
+</table>
+
+### 填充色实例
+
+### 轮廓色实例
+
 ## 阴影
 
 <example-web-graph-style-shadow /> 

+ 639 - 0
docs/setup/centos/app-server/MBI.md

@@ -0,0 +1,639 @@
+# MBI流程
+    上传
+    操作:revit点击 上传按钮
+    
+     模型文件版本检查;
+       检查模型文件版本是否过期
+    加锁检查
+    1,获取所有楼层(F1~F20)
+    2,过滤出更改楼层(计算本地模型文件的Md5值与FileVision.json中本层的Md5值对比,如果不一致表明文件发生更改)
+    3,过滤出加锁的楼层(本人加锁才可以提交)
+    
+    显示 上传模型管理窗口 
+![uploader](./img/mbi_001.png)
+    
+    点击确定上传按钮
+    
+    1,模型与物理世界 信息点同步
+    设备本地编码、设备本地名称、空间本地名称 Revit文件赋值
+    取物理世界信息点的值,覆盖Revit文件中信息点的值。
+    2,获取需要增加、删除、修改的岗位数据
+    3,获取基本关系(设备所在元空间、信标所在元空间)更改。
+    缓存物理世界岗位数据serviceContext:dutys(设备、部件、信标、元空间);
+    由Revit模型文件计算需要上传的岗位数据localContext:localDutys(设备、部件、信标、元空间)+基本关系;
+    
+    岗位数据和基本关系数据整理逻辑
+    输入:物理世界数据缓存serviceContext,本地模型计算计算localContext
+    输出:岗位和基本关系标记(add,del,modify,none)设置正确,准备上传
+    流程:
+    遍历localDutys:localDuty
+      由localDuty的bimid,去dutys集合中查找,赋值给tempDutys;  
+      判断tempDutys集合个数;  
+        等于零;从本地数据库,由Bimd获
+        取dutyId,(待建模型清单产生),跟据dutyId去dutys集合中查找,赋值给tempDutys。
+      再次判断tempDutys集合个数:
+        等于零,说明没有找到物理世界对应的数据;
+        ,此localDuty数据标记为add    
+          处理基本关系    
+        等于1,取第一个duty    
+        大于1,多个岗位共用一个BIMID  ,tempDutys按信息点的个数从大到小排序,取第一个duty,其它标记为Delete    
+        duty.Id赋值给localDuty.Id    
+        判断localDuty.code与duty.code是否相等    
+        不相等,表示族编码发生更改    
+          删除物理世界旧岗位duty标记为del,创建本地新岗位localDuty标记为add,    
+        相等    
+          物理世界旧岗位标duty记为Modify,本地岗位localDuty标记为Modify    
+         处理基本关系    
+    遍历dutys:duty
+    判断duty的状态
+    等于del
+    等于none
+      duty标记为del
+      删除物理世界的相应的基本关系
+    等于modify或者duty中的BIMId格式不正确(待建模清单中的岗位)
+      duty标记为none
+    
+    
+    处理基本关系
+    输入:模型计算结果localContext,岗位localDuty
+    输出:localContext基本关系,标记设置正确
+    流程:
+    判断localDuty类型:
+    空间:
+       更新设备所在元空间、信标所在元空间关系
+        依据元空间BIMID从原始集合equipInSpaceRelations,beaconInSpaceRelations中找到关联的关系集合,tempEquipRelations,tempBeaconRelations
+        判断空间状态
+        add:tempRelations集合状态设置为add
+        del:tempRelations集合状态设置为del
+        Modified:关系删除后增加,tempEquipRelations,tempBeaconRelations集合状态设置为del,复制集合添加至原始集合中标记为add   
+    信标:
+      更新信标所在元空间关系
+         依据信标BIMID从原始集合beaconInSpaceRelations中找到关联的关系集合,tempRelations
+        判断信标状态
+        add:tempRelations集合状态设置为add
+        del:tempRelations集合状态设置为del
+        Modified:关系删除后增加,tempRelations集合状态设置为del,复制集合添加至原始集合中标记为add
+    设备:
+      更新设备所在元空间关系
+        依据设备BIMID从原始集合equipInSpaceRelations中找到关联的关系集合,tempRelations
+        判断空间状态
+        add:tempRelations集合状态设置为add
+        del:tempRelations集合状态设置为del
+        Modified:关系删除后增加,tempRelations集合状态设置为del,复制集合添加至原始集合中标记为add   
+    
+    
+    得到增加,删除,修改的数据(BIMLocation)。
+    删除物理世界的相应的基本关系
+    输入:物理世界数据缓存serviceContext,岗位duty
+    输出:serviceContext基本关系,标记设置正确
+    流程:
+    判断duty类型
+    空间:
+    依据空间,新建关系,设备所在元空间equipInSpace,信标所在元空间beaconInSpace,标记为del
+    将新建的数据添加到集合equipInSpaceRelations,beaconInSpaceRelations
+    信标:
+    依据信标,新建关系,信标所在元空间beaconInSpace,标记为del
+    将新建的数据添加到集合beaconInSpaceRelations
+    设备、部件:
+    依据设备,新建关系,设备所在元空间equipInSpace,标记为del
+    将新建的数据添加到集合equipInSpaceRelations
+    
+    由增加岗位集合,获取增加的基本关系
+    由删除的岗位集合,获取删除的基本关系
+    由修改的岗位集合,先删除,后增加
+    4,与物理世界交互,依据2同步岗位,依据3同步基本关系
+    
+    输入:
+    物理世界项目-建筑-楼层树结构+岗位数据+Revit模型文件
+    输出:
+    待同步的岗位数据+基本关系数据
+    
+    上传Revit模型
+    
+    解锁
+    
+    删除日志文件,删除Revit备份文件
+    
+    上传底图数据(柱子,墙,元空间,设备、虚拟墙)现在没有门窗,想要门窗
+    
+    关闭 上传模型管理窗体
+    
+    空间管理
+    操作:Revit点击:空间管理按钮
+    获取 项目-建筑-楼层 树结构
+    初始化绘图区域
+    显示 空间管理 窗体
+![uploader](./img/mbi_002.png)
+
+    点击 楼层(F1Test)
+    检测是否有未保存的数据,数据分两部分:新建的空间分隔符,删除的空间分隔符
+    弹出提示信息 “尚未保存空间变化,确认放弃修改?”
+    执行初始化楼层
+    查找以-saga结尾的平面视图。如果不存在弹出提示信息“无法找到名称以-saga结尾的视图!”
+    加载Revit模型数据(墙,虚拟墙,柱子,建筑柱,结构柱)
+    创建空间
+    将空间分隔符转化为房间分隔符
+    获取封闭区域(doc.get_PlanTopology(level, phase);)topologyCircuit
+    回滚创建房间分隔符操作
+    由封闭区域topologyCircuit,获取内部点
+    在指定点创建空间(doc.Create.NewSpace(level, phase, new UV(point.X, point.Y));)
+    处理冲突空间
+    绘制墙,虚拟墙,柱,空间
+    
+    点击 绘制虚拟墙
+    开始绘制
+    状态切换为 正在绘制状态
+    鼠标状态切换为 手形
+    
+    再次点击绘制虚拟墙
+    结束绘制
+    状态切换为 结束绘制状态
+    鼠标状态切换为 鼠标
+    
+    鼠标左键单击 画布
+    绘制
+    如果为首个点,点击点向已有线做投影,记录投影点,添加到Polyline集合
+    如果不为首个点,记录点击点,添加到Polyline集合中
+    
+    鼠标右键单击 画布
+    结束绘制
+    如果为首个点,取消绘制
+    如果不为首个点,点击点向已有线做投影,记录投影点,添加到Polyline集合
+    
+    鼠标点击 保存并更改模型
+    保存模型
+    依据新增和删除线集合,删除和创建空间
+    删除空间
+    有删除线的集合,从原空间分隔符集合中,查找到对应的空间分隔符Revit-id,删除空间分隔符
+    由新增集合,创建空间分隔符。
+    创建空间
+    将空间分隔符转化为房间分隔符
+    获取封闭区域(doc.get_PlanTopology(level, phase);)topologyCircuit
+    回滚创建房间分隔符操作
+    由封闭区域topologyCircuit,获取内部点
+    在指定点创建空间(doc.Create.NewSpace(level, phase, new UV(point.X, point.Y));)
+    处理冲突空间
+    冲突处理逻辑:
+    输入:冲突的空间Revit Id (多个),localDutys
+    输出:得到需要保留的空间Id,删除需要舍弃的空间
+    流程:
+    分别由Revit Id 获取物理世界对应dutyId(多个)
+    判断localDutys集合
+    如果获取的dutyId数量等于大于一,弹出提示框,让用户选择保存那一个空间,删除其它空间。
+    如果获取的dutyId数量等于一,保留此空间,删除其它空间
+    如果获取的dutyId数量等于0,保留RevitId最小的空间,删除其它空间。
+    
+    输入:
+    修改的线集合+Revit模型
+    输出:
+    Revit模型中创建虚拟墙和空间
+    
+    保存模型
+    提示用户上传模型
+    
+    
+    
+    
+    空间拓扑计算-平面
+    1,判断空间计算数据的状态
+    Miss 缺失 重新计算
+    Expire 过期 提示用户选择是否重新计算
+    2,重新计算
+    1,  获取所有的楼层文件
+    2,  缓存楼层下的Doors,Windows,Spaces→ ComputerSpaceItems: ComputerSpaceItem
+    3,  Parse 
+    a)         Spaces→Context. M_SaveSpaces: SgSpace空间计算结果数据结构
+    b)        CurrentFloorParse 平面解析
+    空间转为空间边构件数据结构 Spaces→ flatSpaceItems :SpaceEdgeInfo 
+    依据构件Id进行,flatSpaceItems .groupBy(t.EdgeElement.Id) 
+     合并分组,平行且间距小于两个厚度之和的一半的构件看成一个构件关系relations
+    判断同一分组共用构件的边类型,判断邻接构件是否为虚拟墙,如果是墙是否包含门、窗。
+    形成空间两两的邻接关系,以及邻接构件 spaceRelations
+             整理,将两两关系,转化为一个与其它空间的关系,Dictionary<string, SgSpace>
+    c)         UpFloorParse 立面解析
+    从上到下获取相邻的楼层,两两对比 floorUp和floorDown
+    将floorUp和floorDown的空间投影到一个平面,计算两个平面空间的重叠关系。
+    算法:
+    1.         将凹多边形(至少有一个内角大于180度)拆分成多个凸多边形(内角小于等于180度)
+    拆分凹角
+    a)         取得空间点集合外圈逆时针,内圈顺时针。
+    b)        取Y值最小的点,如果多个,取X值最小的点 做为起点start
+    c)         逆时针取第二个点为end
+    d)        取第三个点point,判断start-end,与point-end点乘是否大于零
+    大于零 end点的内角 角度小于180 以end做为start继续计算
+    小于零 end点的内角 角度大于180 将point 做为end点继续向后计算,如果计算结果大于零,连接start point,形成拆分线
+    等于零 夹角为180
+    拆分后,形成两个多边形
+    再次判断两个多边形是否为均凸多边形,
+![uploader](./img/mbi_003.png)
+    
+      
+     
+    2.         判断凸多边形是否相交采用 分离轴定理
+    两个凸多边形不碰撞,则存在一条直线,两个多边形位于直线两侧
+    遍历两个凸多边形的所有边,两个多边形向指定边的投影点集合,集合不相交。
+    
+    模型检查
+    输入:
+    项目-建筑-楼层 树结构+模型文件+检查规范
+    输出:
+    各个检查项的检查结果
+    楼层平面图命名规范检查;
+    命令名称:SagaCheck
+    命令说明:
+    1.模型中有且仅有一个-saga后缀的楼层平面视图
+    2.带saga标记的楼层平面视图编码符合建模规范要求
+    3.是否缺少RF标高,saga平面所在标高应不大于RF标高
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,  获取所有的平面图名称;floorPlans
+    3,  检查是否有且仅有一个-saga后缀的视图;视图符合楼层名称命令规范;(B1,B1M,B1M1,F1,F1M,F2,F2M1,F2M2,F3,RFM,RF); 检查是否包含RF标高
+    4,  记录检查结果
+    5,  保存
+    单层
+    视图名称:F1-saga
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    楼层缺失检查;
+    命令名称:FloorMiss
+    命令说明:
+        检测范围内的楼层文件对应楼层平面图名称应该连续。
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,  获取楼层的saga标记的平面图名称,
+    3,  按名称编码规则从大到小排序;
+    4,  检测楼层平面图名称是否重复或连续;
+         (B1,B1M,B1M1,F1,F1M,F2,F2M1,F2M2,F3,RFM,RF)
+    多层
+    视图名称:F1-saga
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             多楼层对比检查
+    楼层标高与楼层顺序号检查;
+    命令名称:FloorSequenceCheck
+    命令说明:
+        检查楼层文件当前层标高排序与物理世界楼层顺序号排序是否一致。
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+        2,获取楼层的saga标记的标高h,
+    3,  按标高从大到小排序,依次检查后面的楼层顺序号是否小于前面的,否,则不通过。
+     多层
+    楼层顺序号
+    视图名称:F1-saga
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             多楼层对比检查
+    项目长度单位检查;
+    命令名称:UnitCheck
+    命令说明:
+    项目长度单位是否为毫米(mm)
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,  遍历楼层floors:floor
+    3,  获取floor的项目长度单位,判断单位是否为mm,记录
+    4,  保存
+    单层
+    项目单位
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    轴网检查;
+    命令名称:GridCheck
+    命令说明:
+    检查项目下楼层是否共用同一套轴网,同名的轴网需要重合,或延长线重合。
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,  获取所有楼层floors的所有轴网grids;
+    3,  按轴网名称进行分组groups;
+    4,  判断每组轴网Curve的Origin, Direction是否相等;Origin可能不相等,投影,判断投影点与Origin是否相等
+    多层
+    轴网:轴网类型,Start,End,Middle
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             多楼层对比检查
+    设备族编码检查;
+    命令名称:FamilyNameCheck
+    命令说明:
+    指定的Revit分类(Category)下的所有对象,族名称应该按照数据字典规定格式进行编码(详见sheet页:参考-revit分类)
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2, 获取需要检查的Reivt分类(Category),例如:机械设备,常规模型;categories
+    3, 遍历floors:floor   
+        3.1,遍历categories:分类category
+        3.2,从模型floor中获取每个分类category下的所有对象;familyInstances
+        3.3,按分类族名称进行分组;groups
+        3.4,依次检查groups分组内的族名称,是否为可识别类型。
+    注:设备四位编码;部件六位编码;信标Beacon 
+    参考表:参考-revit分类
+    单层
+    设备族名称:弯头 - ISBIM
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             结果按族名称进行分组,备注显示此族使用到的楼层。
+    部件所在位置检查;
+    命令名称:EquipPartLocationCheck
+    命令说明:
+        部件是否有依赖的设备(数据字典规定,部件必须依赖一个设备)
+    实现逻辑:
+    12,              获取需要检查的楼层;floors
+    13,              遍历floors:floor
+    2.1,获取本层所有的部件parts(族编码为六位的)
+    2.2,遍历parts:part,检查与part关联的设备是否存在(与part实体相交,设备族的四位编码TDLS与部件六位编码的前四位相等TDLSDW)
+    2.3,记录检查结果
+             3,保存
+    单层
+    部件Id:720828
+    部件族名称:TDLSDW-低压配电抽屉
+    关联设备Id:720899
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    柱边界检查;
+    命令名称:ColumnCheck
+    命令说明:
+             结构柱 是否为空间边界(影响空间范围),建模要求是结构柱,墙为建筑墙
+    计算逻辑:
+    1, 获取需要检查的楼层;floors
+    2, 遍历floors:floor
+    2.1,获取本层所有的柱子columns
+    2.2,遍历柱子,检查柱子的实例属性“房间边界”是否选中 
+    2.3,记录检查结果
+             3,保存
+![uploader](./img/mbi_004.png)
+
+    单层
+    族名称:钢管混凝土柱 - 矩形
+    Id:628267
+    
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    构件范围检查;
+    命令名称:ElementRangeCheck
+    命令说明:
+    1,正常情况下构件应该位于当前层和上一层之间;
+    2,如果上一层为夹层,构件可位于当前层和夹层的上一层之前;
+    3,如果当前层是屋顶或屋顶夹层,构件应该位置当前层之上,顶部不约束。
+    实现逻辑:
+    前置条件:设置冗余范围w=500
+    1,获取需要检查的楼层;floors
+    2,遍历floors:floor
+    2.1,获取本层,saga视图的高度hBase,上一层不为夹层的高度hTop,本层和上一层之间的夹层列表hSandwishes
+    2.2,获取本层的柱子(顶部zTop,底部zBase)、空间(顶部zTop,底部zBase)、墙(顶部zTop,底部zBase)、设备(定位点zBase)
+    2.3,判断底部zBase,是否位于(hBase-w,hBase)的范围内,顶部hBase是否位于(hTop-w,hTop)的范围内或位置夹层hSandwishes的(t-w,t)之内
+    2.4,记录检查结果
+    3,保存
+    单层
+    高度冗余:500mm
+    本层标高:3
+    顶部标高集合:6,10
+    构件Id:628267
+    构件族名称:750*500
+    构件范围:(0,10)
+    
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             需要多层对比检查,需要获取当前层的上一层(依据楼层序列号)
+    saga标记位置合法性检查;
+    命令名称:SagaPositionCheck
+    命令说明:
+             推测性检查,指定通过率阈值为80%
+        构件范围检测通过率应不低于80%,低于,-saga标记位置可能不正确
+    实现逻辑:
+    前置条件:进行构件范围检查;获取需要检查的楼层;floors
+    1,  遍历floors:floor
+    2,  获取本层构件范围检查的通过率,与阈值进行对比
+    3,  记录对比结果
+    4,  保存
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             这是一个汇总表,业务层实现??
+    连接件检查;
+    命令名称:ConnectorCheck
+    命令说明:
+    1.判断设备是否有连接件(连接件类型:限定水管和风管);
+    2.有连接件的设备是否都已经连接上
+    (详见sheet页:参考-连接件对照表)
+    实现逻辑:
+    1,获取需要检查的楼层;floors
+    2,读取“获取预定义的连接件类”表;equipConnectorTbs
+    3,遍历floors:floor
+    3.1,获取本层的所有构件(设备,部件),存储在familyInstances中; 
+    3.2, 遍历familyInstances:fi
+    3.2.1,获取构件fi的族编码code; 
+    3.2.2,由code从equipConnectorTbs表中取连接件的定义equipConnector
+    3.2.3,获取fi的水管连接件和风管连接件,依据equipConnector判断连接件是否存在,连接件是否都连接。
+    3.2.4,   记录检查结果。
+    4,保存
+    参考表:参考-连接件对照表
+    单层
+    设备Id
+    族名称
+    
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    系统类型名称检查;
+    命令名称:SystemNameCheck
+    命令说明:
+    模型中系统类型名称与物理世界可识别的拓扑图名称是否一致(详见sheet页:参考-可识别的系统名称)
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,  读取“获取可识别的管道系统名称”表;systemTypeTbs
+    3,  遍历floors:floor
+    2.1,获取本层的所有风管系统和水管系统,存储在mepSystemTypes中; 
+    2.2, 遍历mepSystemTypes:mepSystemType
+    2.2.1,依据mepSystemType的名称,从systemTypeTbs中查找,判断是否存在同名系统, 
+    2.2.2,记录判断结果
+    4,  保存
+    参考表:参考-可识别的系统名称
+    单层
+    系统名称
+    系统类型
+    
+    
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             需要参考表
+    未在空间中的设备;
+    命令名称:EquipInSpaceCheck
+    命令说明:
+        原则上设备和部件都应该在空间中,导出没有空间中的设备,部件
+    实现逻辑:
+    1,  获取需要检查的楼层;floors
+    2,              遍历floors:floor
+    2.1,获取本层的所有可识别的构件familyInstances; 
+    2.2, 遍历familyInstances:fi
+    2.2.1,获取fi所在的空间space,如果 space存在,记录空间id,否则不通过检查
+    2.2.2,记录判断结果
+    3,              保存
+    单层
+    族名称:EGCR-门禁-带键盘读卡器
+    Id:1530269
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    管网及相关设备检查;
+    命令名称:SystemReferEquipCheck
+    命令说明:
+        检查设备连接的管道系统是否与规范一致。规范(详见sheet页:参考-管网及相关设备)
+    实现逻辑:
+    1,获取需要检查的楼层;floors
+    2, 读取“设备关联的系统类型”表;equipSystemTbs
+    3,  遍历floors:floor
+    2.1,获取本层的所有可识别的构件familyInstances; 
+    2.2, 遍历familyInstances:fi
+    2.2.1,获取构件fi的族编码(设备四位或部件六位,TDLS,TDLSDW);code
+    2.2.2,由code,从equipSystemTbs表中取,此code允许连接的系统类型equipSystems;
+    2.2.3,取构件fi在模型中连接的系统connectSystems;
+    2.2.4,connectSystems与equipSystems求差集和交集,得到未通过检查的系统名称unPressSystems和通过检查的系统名称pressSystems;
+    2.2.5,记录判断结果
+    4, 保存
+    需要参考表:参考-管网及相关设备
+    单层
+    族名称:EGCR-门禁-带键盘读卡器
+    Id:1530269
+    通过检查的系统名称:冷却水供水管 4,冷却水回水管 2
+    未通过检查的系统名称:冷冻水回水管 2,冷冻水供水管 8
+    
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             需要参照表
+    Revit族参数完整性检查;
+    命令名称:ParameterIntegrityCheck
+    命令说明:
+             检查设备族参数“设备本地名称”、“设备本地编码”是否缺失
+    实现逻辑:
+    1,获取需要检查的楼层;floors
+    2,遍历floors:floor
+    2.1,获取本层的所有可识别的构件familyInstances; 
+    2.2,对familyInstances依据族名称进行分组,familyNameGroups
+    2.2, 遍历familyNameGroups,取分组集合中的第一构件,判断是否包含“设备本地名称”,“设备本地编码”参数。
+    2.3,记录判断结果
+    3,保存
+    单层
+    族名称:EGCR-门禁-带键盘读卡器
+    Id:1530269
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    垃圾数据检查
+    输入:
+    项目-建筑-楼层 树结构+物理世界岗位数据+Revit模型文件
+    输出:
+    各个检查项的检查结果
+    资产与设备信息不一致;
+    命令名称:无
+    命令说明:
+             检查资产与设备同一信息点都有值,值是否相等
+    实现逻辑:
+    1,  缓存Building下的所有资产;
+    2,  缓存Building下的所有岗位;
+    遍历所有的岗位,由岗位获取其对应的资产,对比岗位和资产信息点,判断信息点定义相同的信息点,如果有值,值是否相等。
+    返回值示例:无
+    备注:
+             与物理世界交互,与Revit模型无关
+    
+     输入:项目-建筑-楼层 树结构+物理世界岗位数据+扫楼后台资产数据
+     输出:岗位和资产不致的信息点集合
+    物理世界中资产岗位关系与二维码中存的关系一致性检查;
+    命令名称:无
+    命令说明:
+        二维码照片存储的资产和岗位是否正确
+    实现逻辑:
+    1,  获取Buiding下的所有资产;
+    2,  缓存二维码照片关系表信息(saas, qrcode/restQRCodeService/queryQRCodeByFmIds)
+    关系表中存储的数据:key,fmId,dutyId,projectId
+    3,  判断资产是否存在对应岗位,如果存在检查下一条资产,不存在,进行下面判断
+    4,  由资产id,查找二维码照片关系表中;是否存在对应资产的关系;如果不存在,检查下一条资产,存在,进行下面判断
+    5,  对比资产对应的岗位id和二维码关系中objId,是否相等。
+    返回值示例:无
+    备注:
+             与物理世界交互,与Revit模型无关
+     
+     输入:项目-建筑-楼层 树结构+扫楼资产数据+saas二维码关系数据
+     输出:二维码中岗位与资产中岗位是否一致的检查集合
+    
+    删除此命令
+    物理世界对象与模型一致性检查;
+    命令名称:MissDutyOrMode
+    命令说明:
+    模型√ 对象×  需要重新创建对象
+    模型×  对象√ 需要删除对象
+    实现逻辑:
+    1,  获取Building下所有的楼层;
+    2,  遍历楼层;
+    3,  缓存本层物理世界的所有对象(设备、部件、信标、空间)数据;A
+    4,  由本层模型计算所有可识别对象(设备、部件、信标、空间)数据;B
+    5,  3,4数据分别求差集,得到新增和删除对象的数据;
+    6,  移除删除的对象中BIMID不完整的数据。(通过台账创建的岗位或模型文件替换)
+    7,  导出新增和删除对象的列表。
+        注:BIMID完整示例:Fl1223xxx:1233; 
+        A与B求差集,得到需要删除的集合
+        B与A求差集,得到需要增加的集合
+    返回值示例:
+    图片: https://shimo.im/fake.png
+    备注:
+             此处仅返回从模型中取出的数据,后续需要从物理世界取数据,对比得结果。
+    
+     输入:项目-建筑-楼层 树结构+物理世界岗位数据+Revit模型文件
+     输出:需要删除的岗位集合和需要新增的岗位集合
+    多个岗位共用一个bimID;
+    命令名称:无
+    命令说明:
+             多个岗位共用一个bimID
+    实现逻辑:
+    1,  获取当前项目下的所有建筑(Building)A
+    2,  遍历A,
+    3,  获取Building下所有物理世界的对象;(设备、部件,信标,空间)
+    4,  按BIMID进行分组;
+    5,  遍历分组,如果分组中对象数量大于1,且对象的BIMID不为空且不为楼层Id,表明此分组对象共用一个BIMID;
+        注:对象的BIMID为空或为楼层Id表示些对象为待建模状态。
+    返回值示例:无
+    备注:
+             与物理世界交互,与Revit模型无关
+     
+     输入:项目-建筑-楼层 树结构+物理世界岗位数据
+     输出:岗位共用一个BIMId异常数据集合
+    与业务空间有所在关系的设备的实际位置和业务空间位置不一致检查;
+    命令名称:无
+    命令说明:
+    业务空间所在楼层和其中的设备所在楼层一致性检查   
+    实现逻辑:
+    1,  获取所有楼层;
+    2,  获取所有的业务空间类型的集合,得到BusinessSpaceTypes(data-platform-3/infocode/business_space_type/query)
+    3,  遍历楼层:
+    3.1,查询楼层下的设备,得到设备集合FloorEquips;
+    3.2,遍历业务空间类型集合BusinessSpaceTypes; businessSpaceType
+    3.3,获取本层businessSpaceType类型的业务空间,得到空间集合businessSpaces;
+    3.4,获取与空间关联的设备集合,得到设备集合:businessSpaceEquips
+    3.5,设备集合FloorEquips与businessSpaceEquips 求差集,得到设备集合:exceptEquips(即设备所在楼层与业务空间所在楼层不一致的设备); 
+        4,输出设备集合:exceptEquips。
+     
+    返回值示例:无
+    备注:
+             与物理世界交互,与模型无关
+    
+     输入:项目-建筑-楼层 树结构+物理世界业务空间类型集合+物理世界业务空间集合+业务空间与设备的图关系+楼层下的设备集合
+     输出:业务空间中的设备的楼层与设备自身楼层不一致的集合
+    revit模型中的族编码和岗位的设备类编码一致性检查;
+    命令名称:无
+    命令说明:
+    1.       模型中的族编码和岗位的设备类编码是否一致
+    实现逻辑:
+    1,  获取所有楼层;得到楼层集合:floorList;
+    2,  遍历floorList,floor
+    2.1,获取物理世界floor下的所有设备和部件集合,得到岗位集合dutyList; 
+    2.2,遍历dutyList:duty
+          2.2.1,由duty.BIMId,从Revit模型文件中获取模型对象element
+          2.2.2,对比岗位中编码与模型文件中获取的编码对比;
+          2.2.3,如果编码不一致,保存到错误集合。
+     
+    返回值示例:
+    
+     输入:项目-建筑-楼层 树结构+物理世界的岗位数据+Revit模型文件
+     输出:岗位编码与模型编码不一致的集合
+    
+    

+ 139 - 0
docs/setup/centos/app-server/centos-postgresql-plpython3.md

@@ -0,0 +1,139 @@
+# centos7/postgresql(安装pl/python3扩展插件, 编写pl/python3方法)
+
+
+## 安装python3
+
+    1. 安装相关包: 
+        yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make
+        yum install libffi-devel -y
+        cd /usr/bin
+    
+    2. 备份旧python命令(软连接): 
+        mv python python.bak
+        cd
+    
+    3. 下载压缩包: wget https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tar.xz
+    解压: tar -xvJf Python-3.7.3.tar.xz
+    
+    4. 编译安装:./configure --enable-shared --prefix=/usr/local/python3 --enable-optimizations LDFLAGS="-Wl,--rpath=/usr/local/python3/lib"
+        或编译安装(非linux版本, 此处不选用): ./configure --enable-shared --prefix=/usr/local  LDFLAGS="-R/usr/local/lib"
+    make && make install
+    cd /usr/bin
+    制作软连接: ln -s /usr/local/python3/bin/python3 /usr/bin/python
+    
+    5. 检查版本: python -V
+    因为yum使用python2, 所以修改yum使用python版本为2:
+    vi /usr/bin/yum
+    把#! /usr/bin/python修改为#! /usr/bin/python2
+    同理 vi /usr/libexec/urlgrabber-ext-down 文件里面的#! /usr/bin/python 也要修改为#! /usr/bin/python2
+    
+    这样python3版本就安装完成;同时python2也存在
+    python -V   版本3 
+    python2 -V 版本2
+    
+    
+## 编译安装postgresql(Centos7)
+
+    1. 编译环境和下载安装包
+        更新yum: yum update
+        安装wget: yum install wget
+        下载源码: wget https://ftp.postgresql.org/pub/source/v11.3/postgresql-11.3.tar.gz
+        解压: tar zxvf postgresql-11.3.tar.gz
+        安装编译需要的命令库: yum install gcc   yum install readline-devel    yum install zlib-devel
+       
+    2. 编译安装
+        ./configure --with-python
+        make (确保执行完最后一行出现All of PostgreSQL successfully made. Ready to install.)
+        make install (最后一行出现PostgreSQL installation complete.为正常安装完成)
+      
+    3. 创建用户并授权
+        useradd postgres
+        chown -R postgres:postgres /usr/local/pgsql
+        
+        
+    4. 配置环境变量
+        切换到用户: su - postgres
+        打开.bash_profile: vi ~/.bash_profile
+        
+![image](./img/plpython3_001.png)
+        
+        修改为如下:
+            PGHOME=/usr/local/pgsql
+            export PGHOME
+            PGDATA=/usr/local/pgsql/data
+            export PGDATA
+             
+            PATH=$PATH:$HOME/.local/bin:$HOME/bin:$PGHOME/bin
+            export PATH
+            
+        修改完执行: source ~/.bash_profile
+        
+    5. 初始化数据库
+        执行: initdb(初始化完毕后有提示开启服务的命令)
+        开启数据库服务: pg_ctl -D /usr/local/pgsql/data -l logfile start     (关闭 stop/重启restart)
+        查看版本: psql -V
+        进入控制台: psql
+        进入控制台后修改密码 \password postgres
+        
+    6. 配置连接规则
+        在/usr/local/pgsql/data下, 有两个配置文件需要修改:
+        postgresql.conf文件中, 需要配置
+        
+            listen_addresses = '*'     # 监听规则, *为监听所有
+            
+        pg_hba.conf文件中, 配置访问规则
+        最后面加入一行
+        
+            host   all    all    0.0.0.0/0   trust
+            
+        详细配置可参考: 
+        (https://www.cnblogs.com/flying-tiger/p/5983588.html)
+        官网: https://www.postgresql.org/docs/current/auth-pg-hba-conf.html
+        注: 比如对于IPv4地址来说, 172.20.143.89/32指定单个主机的IP,172.20.143.0/24代表一个小的子网。
+            对于IPv6地址来说,::1/128指定单个主机(这里是本机环回地址),fe80::7a31:c1ff:0000:0000/96 指定一个IPv6的子网。
+            0.0.0.0/0代表所有IPv4地址,::0/0代表所有IPv6地址。
+        配置完毕之后, 需要重启数据库服务方可生效.
+    
+    7. 启用并使用pl/python(plpython3u), create function
+        1). 使用数据库浏览工具(比如navicat, pgAdmin4)连接刚配置完成的数据库
+        2). 执行: create language plpython3u;  即创建对于python3的支持
+        3). 如果需要import的库不存在, 则可以在python3/bin目录下执行pip3命令安装需要的库
+            (例如: python pip3 install matplotlib 或./pip3 install matplotlib, 下面例子需要用到, 
+            安装一般在python3目录下的lib/python3.7/site-packages目录下, pip3 show matplotlib可查)
+        4). pip3 install matplotlib -i   
+        5). 可以创建一个数据库function, 判断一个点是否在一个由多个点依次顺时针组成的面内.
+        
+## 示例 (注意: 输入参数名不能有大写字母)
+    
+    CREATE OR REPLACE FUNCTION is_point_in_polygon(IN the_point jsonb, IN the_polygon jsonb) RETURNS boolean AS
+    $$
+    from matplotlib.path import Path
+    import json
+    
+    
+    points = []
+    poly = []
+    a_point = json.loads(the_point)
+    a_polygon = json.loads(the_polygon)
+    points.append(a_point["x"])
+    points.append(a_point["y"])
+    points = tuple(points)
+    
+    l = len(a_polygon)
+    for i in range(l):
+        ele = a_polygon[i]
+        single_point = []
+        single_point.append(ele["x"])
+        single_point.append(ele["y"])
+        poly.append(tuple(single_point))
+    p = Path(poly)
+    return p.contains_points([points])
+    $$
+    LANGUAGE 'plpython3u' VOLATILE;
+    
+    
+    验证
+    select is_point_in_polygon('{"x":1.2, "y":3}', '[{"x":1.1, "y":1.1},{"x":1.1, "y":4.1},{"x":4.1, "y":4.1},{"x":4.1, "y":1.1}]')
+    
+    select is_point_in_polygon('{"x":1.1, "y":3}', '[{"x":1.1, "y":1.1},{"x":1.1, "y":4.1},{"x":4.1, "y":4.1},{"x":4.1, "y":1.1}]')
+    

BIN
docs/setup/centos/app-server/img/mbi_001.png


BIN
docs/setup/centos/app-server/img/mbi_002.png


BIN
docs/setup/centos/app-server/img/mbi_003.png


BIN
docs/setup/centos/app-server/img/mbi_004.png


BIN
docs/setup/centos/app-server/img/plpython3_001.png


BIN
docs/setup/centos/app-server/img/revit_001.jpg


BIN
docs/setup/centos/app-server/img/revit_002.jpg


BIN
docs/setup/centos/app-server/img/revit_003.jpg


BIN
docs/setup/centos/app-server/img/scheduler_001.jpg


+ 57 - 0
docs/setup/centos/app-server/revit-algorithm.md

@@ -0,0 +1,57 @@
+# Revit-algorithm
+## 项目结构
+![项目结构](./img/revit_001.jpg)
+
+    如图中所标记
+    1. 后台任务模块, 处理模型的各个状态和状态流转
+    2. 配置类
+    3. 对外的服务接口
+    4. 所有的模型类
+    5. 所有的服务类
+    6. 项目配置文件
+    7. 测试类
+    8. Gradle的build文件
+    
+## 项目介绍
+
+### 功能
+    1. 提供上传, 管理revit模型和通知的服务
+    2. 提供检查, 校验并处理模型, 导出revit数据同步到数据中心的服务
+    3. 提供模型底图服务
+    
+### 技术细节
+    1. 基于springboot web框架
+    2. 数据库使用postgresql, 数据持久化框架使用myBatis和sybotan(公司内部)
+    3. 检查标高和轴网使用postgresql中的扩展插件plpython3
+    4. 发送JMS消息使用activemq
+    5. 使用poi操作xlsx文件
+    
+## 打包
+![项目打包](./img/revit_002.jpg)
+
+    如果打测试包, 在右侧Gradle的管理页面里双击任务buildDevJar或者buildDevWar即可. 打包内容中就会
+    把左侧资源文件包resources和resources-dev的内容加入到打包的资源内, 建议在打包前把上次打包的文件删除
+    
+
+## 服务依赖
+    1. 依赖于数据中心
+    2. 依赖于模型调度器
+    3. 依赖于文件服务器(必须具备分页上传的功能)
+    
+    
+    
+## 文件服务配置
+![项目打包](./img/revit_003.jpg)
+    
+    
+| 配置            | 描述                             |
+| --------        | :----:                          |
+| url             |   文件服务的基础url              |
+| systemId        |   文件服务的systemId用户名       |
+| secret          |   文件服务的密码(与用户名匹配)    |
+| dir             |   底图存放目录                   |
+| report          |   错误报告存放目录               |
+| export          |   模型导出json压缩数据存放目录    |
+
+
+    

+ 540 - 0
docs/setup/centos/app-server/revit-check.md

@@ -0,0 +1,540 @@
+# RevitDoc--模型检查数据格式
+
+文档中单位统一使用公制,英制需转为公制使用
+
+    数据检查的总体格式
+ 
+    {
+        "Content":[{checkItem0},{checkItem1}...],
+        "FloorName":"D:\RevitWorkSpace\Pj1101080004\Bd110108000438d0a8c0d6e843ef85d739e91f1c9db6\Fl11010800043637f04b70834f2dbe0b82eb39068a04.rvt",
+        "Result":"Success"
+    }
+补充:ResultMsg为null或”“时不显示
+
+## 楼层平面图命名规范检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+
+    {
+        "Name":"SagaCheck",
+        "Content":[
+            {
+                "PlanName":"F1-saga",
+                "Result":"Success"
+            }
+        ],
+        "Result":"Success"
+    }
+    
+| 名称              | 解释           | 示例                      |
+|-------------------|----------------|---------------------------|
+| Content.PlanName  | 楼层平面图名称 | F1-saga                   |
+| Content.Result    | 检查结果       | 可选:“Success”,"Failure" |
+| Content.ResultMsg | 提示信息       | 为""或null,不显示此项    |
+| Name              | 检查项名称     | "SagaCheck"               |
+| ResultMsg         | 提示信息       | 为""或null,不显示此项    |
+| Result            | 结果           | “Success”,"Failure"       |
+
+## 项目长度单位检查
+
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"UnitCheck",
+        "Content":[
+            {
+                "Unit":"DUT_MILLIMETERS",
+                "Result":"Success",
+                "ResultMsg":""
+            }
+        ],
+        "Result":"Success"
+    }
+
+| 名称              | 解释         | 示例                      |
+|-------------------|--------------|---------------------------|
+| Content.FloorUnit | 项目长度单位 | mm                        |
+| Content.Result    | 检查结果     | 可选:“Success”,"Failure" |
+| Content.ResultMsg | 提示信息     | 为""或null,不显示此项    |
+| Name              | 检查项名称   | "UnitCheck"               |
+| ResultMsg         | 提示信息     | 为""或null,不显示此项    |
+| Result            | 结果         | “Success”,"Failure"       |
+| Result            | 结果         | “Success”                 |
+
+## 轴网检查
+    所需要的数据,在提取时提取。
+
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Contents":[
+            {
+                "GridType":"Arc",
+                "GridStart":"688.217416431504, 53.8058987149525, 0",
+                "GridEnd":"688.217416431504, 53.8058987149525, 0",
+                "GridMiddle":"688.217416431504, 53.8058987149525, 0"
+            }
+        ]
+    }
+| 名称               | 解释       | 示例                                    |
+|--------------------|------------|-----------------------------------------|
+| FloorName          | 楼层名称   | PjXXX_001                               |
+| Content.GridType   | 轴网类型   | Arc,Line                                |
+| Content.GridStart  | 轴网起点   | ”688.217416431504, 53.8058987149525, 0“ |
+| Content.GridEnd    | 轴网终点   | ”688.217416431504, 53.8058987149525, 0“ |
+| Content.GridMiddle | 轴网中心点 | ”688.217416431504, 53.8058987149525, 0“ |
+| ResultMsg          | 提示信息   | “”                                      |
+| Result             | 结果       | “Success”                               |
+
+
+## 设备族编码检查
+
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"FamilyNameCheck",
+        "Content":[
+            {
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "Result":"Success"
+            },
+            ...
+        ],
+        "ReferenceSheet":"参考-revit分类",
+        "Result":"Success"
+    }
+    
+| 名称               | 解释       | 示例                      |
+|--------------------|------------|---------------------------|
+| Content.FamilyName | 族名称     | ATFC-带回风箱的风机盘管   |
+| Content.Result     | 检查结果   | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息   | 为""或null,不显示此项    |
+| Name               | 检查项名称 | "FamilyNameCheck"         |
+| ResultMsg          | 提示信息   | 为""或null,不显示此项    |
+| Result             | 结果       | “Success”,"Failure"       |
+| Result             | 结果       | “Success”                 |
+
+##柱边界检查
+
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"ColumnCheck",
+        "Content":[
+            {
+                "Id":"316183",
+                "FamilyName":"混凝土 - 矩形 - 柱",
+                "Result":"Success"
+            },
+            ...
+        ],
+        "Result":"Success"
+    }
+    
+| 名称               | 解释        | 示例                      |
+|--------------------|-------------|---------------------------|
+| Content.FamilyName | 柱名称      | 混凝土 - 矩形 - 柱        |
+| Content.BIMID      | Revit实例Id | 316183                    |
+| Content.Result     | 检查结果    | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息    | 为""或null,不显示此项    |
+| Name               | 检查项名称  | "ColumnCheck"             |
+| ResultMsg          | 提示信息    | 为""或null,不显示此项    |
+| Result             | 结果        | “Success”,"Failure"       |
+
+## 部件所在位置检查
+
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"EquipPartLocationCheck",
+        "Content":[
+            {
+                "PartFamilyName":"TDLSDW-低压配电抽屉",
+                "Result":"Success",
+                "ResultMsg":"通过"
+            }
+        ]
+    }
+| 名称                   | 解释       | 示例                      |
+|------------------------|------------|---------------------------|
+| Content.PartFamilyName | 部件族名称 | TDLSDW-低压配电抽屉       |
+| Content.Result         | 检查结果   | 可选:“Success”,"Failure" |
+| Content.ResultMsg      | 提示信息   | 为""或null,不显示此项    |
+| Name                   | 检查项名称 | "EquipPartLocationCheck"  |
+| ResultMsg              | 提示信息   | 为""或null,不显示此项    |
+| Result                 | 结果       | “Success”,"Failure"       |
+| Result                 | 结果       | “Success”,"Failure"       |
+## 连接件检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"ConnectorCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "Type":"设备",
+                "ResultMsg":"发现 水管连接件未连接;发现 风管连接件未连接;",
+                "Result":"Failure"
+            },
+            ...
+        ],
+        "ReferenceSheet":"参考-连接件对照表",
+        "Result":"Success"
+    }
+| 名称               | 解释        | 示例                      |
+|--------------------|-------------|---------------------------|
+| Content.Id         | Revit实例Id | 363043                    |
+| Content.FamilyName | 构件族名称  | 混凝土 - 矩形 - 柱        |
+| Content.Type       | 构件类型    | 设备,部件                 |
+| Content.Result     | 检查结果    | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息    | 为""或null,不显示此项    |
+| Name               | 检查项名称  | "ConnectorCheck"          |
+| ReferenceSheet     | 关联表      | 参考-连接件对照表         |
+| ResultMsg          | 提示信息    | 为""或null,不显示此项    |
+| Result             | 结果        | “Success”,"Failure"       |
+
+## 构件范围检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Redundant":500,
+        "BaseLevel":0,
+        "TopLevels":"4500",
+        "Name":"ElementRangeCheck",
+        "Content":[
+            {
+                "Id":"316183",
+                "FamilyName":"混凝土 - 矩形 - 柱",
+                "Type":"柱",
+                "HeightRange":"0,4500",
+                "ResultMsg":"",
+                "Result":"Success"
+            },
+            ...
+        ],
+        "Result":"Success"
+    }
+
+| 名称                | 解释                                                   | 示例                          |
+|---------------------|--------------------------------------------------------|-------------------------------|
+| Content.Id          | Revit实例Id                                            | 316183                        |
+| Content.FamilyName  | 构件族名称                                             | 混凝土 - 矩形 - 柱            |
+| Content.Type        | 构件类型                                               | 柱,墙,空间,设备,部件,信标 |
+| Content.HeightRange | 高度范围                                               | "0,4500"                      |
+| Content.Result      | 检查结果                                               | 可选:“Success”,"Failure"     |
+| Content.ResultMsg   | 提示信息                                               | 为""或null,不显示此项        |
+| Name                | 检查项名称                                             | "ElementRangeCheck"           |
+| Redundant           | 构建检查的冗余值                                       | 500                           |
+| BaseLevel           | 当前层标记                                             | 0                             |
+| TopLevels           | 顶部标记集合,可能包含多个夹层和上一层,多个使用;分割 | "4500"                        |
+| ResultMsg           | 提示信息                                               | 为""或null,不显示此项        |
+| Result              | 结果                                                   | “Success”,"Failure"           |
+## 系统类型名称检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"SystemNameCheck",
+        "Content":[
+            {
+                "SystemName":"送风",
+                "SystemType":"风管系统",
+                "ResultMsg":"未知的系统名称,请按照系统类型命名规范修改",
+                "Result":"Failure"
+            },
+    ...
+        ],
+        "ReferenceSheet":"参考-可识别的系统名称",
+        "Result":"Success"
+    }
+| 名称               | 解释       | 示例                      |
+|--------------------|------------|---------------------------|
+| Content.Systemname | 系统名称   | 雨水管                    |
+| Content.Type       | 构件类型   | 管道系统;风管系统        |
+| Content.Result     | 检查结果   | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息   | 为""或null,不显示此项    |
+| Name               | 检查项名称 | "SystemNameCheck"         |
+| ReferenceSheet     | 关联表     | 参考-可识别的系统名称     |
+| ResultMsg          | 提示信息   | 为""或null,不显示此项    |
+| Result             | 结果       | “Success”,"Failure"       |
+## 未在空间中的设备
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"EquipInSpaceCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "SpaceId":"445757",
+                "Result":"Success"
+            },
+            ...
+        ],
+        "Result":"Success"
+    }
+| 名称               | 解释         | 示例                      |
+|--------------------|--------------|---------------------------|
+| Content.Familyname | 族名称       | FSCP-消火栓起泵按钮       |
+| Content.Id         | Revit实例Id  | 1530697                   |
+| Content.SpaceId    | 关联空间的Id | 445757                    |
+| Content.Result     | 检查结果     | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息     | 为""或null,不显示此项    |
+| Name               | 检查项名称   | "EquipInSpaceCheck"       |
+| ResultMsg          | 提示信息     | 为""或null,不显示此项    |
+| Result             | 结果         | “Success”,"Failure"       |
+
+## 管网及相关设备检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"SystemReferEquipCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "PassSystems":"",
+                "UnPassSystems":"送风、回风",
+                "ResultMsg":"连接件所连接的管道系统与规范不一致,请检查",
+                "Result":"Failure"
+            },
+            ...
+        ],
+        "ReferenceSheet":"参考-管网及相关设备",
+        "Result":"Success"
+    }
+| 名称                  | 解释                 | 示例                      |
+|-----------------------|----------------------|---------------------------|
+| Content.Id            | Revit实例Id          | 1530697                   |
+| Content.Familyname    | 族名称               | FSCP-消火栓起泵按钮       |
+| Content.PassSystems   | 通过检查的管网系统   | 循环供水、湿式消防系统    |
+| Content.UnPassSystems | 未通过检查的管网系统 | 其他                      |
+| Content.Result        | 检查结果             | 可选:“Success”,"Failure" |
+| Content.ResultMsg     | 提示信息             | 为""或null,不显示此项    |
+| Name                  | 检查项名称           | "SystemReferEquipCheck"   |
+| ResultMsg             | 提示信息             | 为""或null,不显示此项    |
+| Result                | 结果                 | “Success”,"Failure"       |
+## Revit族参数完整性检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"ParameterIntegrityCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "ResultMsg":"缺失的参数为:设备本地名称、设备本地编码",
+                "Result":"Failure"
+            },
+           ...
+        ],
+        "Result":"Success"
+    }
+| 名称               | 解释        | 示例                             |
+|--------------------|-------------|----------------------------------|
+| Content.Id         | Revit实例Id | 1530697                          |
+| Content.Familyname | 族名称      | VTIO-送风散流器 - 方形(200x200) |
+| Content.Result     | 检查结果    | 可选:“Success”,"Failure"        |
+| Content.ResultMsg  | 提示信息    | 为""或null,不显示此项           |
+| Name               | 检查项名称  | "ParameterIntegrityCheck"        |
+| ResultMsg          | 提示信息    | 为""或null,不显示此项           |
+| Result             | 结果        | “Success”,"Failure"              |
+
+## 管段检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"PipeCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "SystemName":"给水管",
+                "ResultMsg":"末端未连接任何设备,请检查",
+                "Result":"Failure"
+            }
+        ],
+        "Result":"Success"
+    }
+| 名称               | 解释           | 示例                      |
+|--------------------|----------------|---------------------------|
+| Content.Id         | Revit实例Id    | 1530697                   |
+| Content.SystemName | 管道系统的名称 | 给水管                    |
+| Content.Result     | 检查结果       | 可选:“Success”,"Failure" |
+| Content.ResultMsg  | 提示信息       | 为""或null,不显示此项    |
+| Name               | 检查项名称     | PipeCheck Pip""           |
+| ResultMsg          | 提示信息       | 为""或null,不显示此项    |
+| Result             | 结果           | “Success”,"Failure"       |
+
+
+## xyz坐标重叠检查
+    输入:Revit模型文件地址 ***\F1.rvt
+    输出:
+    {
+        "Name":"XYZOverlapCheck",
+        "Content":[
+            {
+                "Id":"363043",
+                "FamilyName":"ATFC-带回风箱的风机盘管",
+                "ResultMsg":"与340021的坐标发生重叠,请检查",
+                "Result":"Failure"
+            },
+           ...
+        ],
+        "Result":"Success"
+    }
+
+| 名称               | 解释        | 示例                             |
+|--------------------|-------------|----------------------------------|
+| Content.Id         | Revit实例Id | 1530697                          |
+| Content.Familyname | 族名称      | VTIO-送风散流器 - 方形(200x200) |
+| Content.Result     | 检查结果    | 可选:“Success”,"Failure"        |
+| Content.ResultMsg  | 提示信息    | 为""或null,不显示此项           |
+| Name               | 检查项名称  | "XYZOverlapCheck"                |
+| ResultMsg          | 提示信息    | 为""或null,不显示此项           |
+| Result             | 结果        | “Success”,"Failure"              |
+
+## 完整示例
+    {
+        "FloorName":"F1.rvt",
+        "Result":"Success",
+        "ResultMsg":"",
+        "Content":[
+            {
+                "Name":"SagaCheck",
+                "Content":[
+                    {
+                        "PlanName":"F1-saga",
+                        "Result":"Success",
+                        "ResultMsg":""
+                    }
+                ]
+            },
+            {
+                "Name":"UnitCheck",
+                "Content":[
+                    {
+                        "Unit":"mm",
+                        "Result":"Success",
+                        "ResultMsg":""
+                    }
+                ]
+            },
+            {
+                "Name":"FamilyNameCheck",
+                "Content":[
+                    {
+                        "FamilyName":"弯头 - ISBIM",
+                        "Result":"Failure",
+                        "ResultMsg":"请检查族名称编码是否符合要求"
+                    }
+                ]
+            },
+            {
+                "Name":"EquipPartLocationCheck",
+                "Content":[
+                    {
+                        "PartId":"720828",
+                        "PartFamilyName":"TDLSDW-低压配电抽屉",
+                        "RefId":"720899",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"ColumnCheck",
+                "Content":[
+                    {
+                        "Id":"628267",
+                        "FamilyName":"钢管混凝土柱 - 矩形",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"ElementRangeCheck",
+                "Redundant":500,
+                "BaseLevel":0,
+                "TopLevels":"6;9;10",
+                "Content":[
+                    {
+                        "Id":"720828",
+                        "FamilyName":"VTSF-混流式风机",
+                        "Type":"设备",
+                        "HeightRange":"3,10",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"ConnectorCheck",
+                "Content":[
+                    {
+                        "Id":"628267",
+                        "FamilyName":"VTSF-混流式风机",
+                        "Result":"Failure",
+                        "ResultMsg":"发现 风管连接件未连接;"
+                    }
+                ]
+            },
+            {
+                "Name":"SystemNameCheck",
+                "Content":[
+                    {
+                        "SystemName":"回风风管",
+                        "SystemType":"风管系统",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"EquipInSpaceCheck",
+                "Content":[
+                    {
+                        "Id":"1530269",
+                        "FamilyName":"EGCR-门禁-带键盘读卡器",
+                        "Result":"Failure",
+                        "ResultMsg":"请检查设备是否在空间中"
+                    }
+                ]
+            },
+            {
+                "Name":"SystemReferEquipCheck",
+                "Content":[
+                    {
+                        "Id":"1530269",
+                        "FamilyName":"EGCR-门禁-带键盘读卡器",
+                        "PassSystems":"冷却水供水管 4,冷却水回水管 2",
+                        "UnPassSystems":"冷冻水回水管 2,冷冻水供水管 8",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"ParameterIntegrityCheck",
+                "Content":[
+                    {
+                        "FamilyName":"EGCR-门禁-带键盘读卡器",
+                        "Result":"Success",
+                        "ResultMsg":"通过"
+                    }
+                ]
+            },
+            {
+                "Name":"PipeCheck",
+                "Content":[
+                    {
+                        "Id":"363043",
+                        "SystemName":"给水管",
+                        "ResultMsg":"末端未连接任何设备,请检查",
+                        "Result":"Failure"
+                    }
+                ],
+                "Result":"Success"
+            }
+        ]
+    }

+ 876 - 0
docs/setup/centos/app-server/revit-format.md

@@ -0,0 +1,876 @@
+#RevitDoc--提取数据格式
+
+
+
+点信息格式为:
+
+    {
+        "X":-5498.19,
+        
+        "Y":7008.54,
+        
+        "Z":0
+    }
+
+小数点后保留两位,单位为公制
+
+OutLine为点集合,围成闭合区域,注意将起始点添加到集合中
+
+项目
+
+输入:Revit模型文件地址 \F1.rvt
+
+输出:
+
+    {
+    
+        "MBIName":"PjXXX_001",
+    
+        "PlanName":"F1-saga",
+    
+        "Parameters":[
+    
+        ],
+    
+        "Elements":[
+    
+        ]
+    
+    }
+
+  ------------ ---------------------------------------------------------------------------------------------------------------------------------------------- ------------------
+  |名称         |解释                                                                                                                                           |示例|
+  |--------|--------|-------------------|
+  |MBIName      |BIM模型中存储的对应物理世界的文件序号                                                                                                          | PjXXX_001|
+  |PlanName     |当前楼层代表的平面图名称                                                                                                                       | F1-saga |
+  |Parameters   |从BIM模型中读取到的参数,目前值为空                                                                                                            | 格式参见参数定义 |
+  |Elements     |BIM模型中包含的构件 类型的分组:Units,Levels,Grids,Walls,VirtualWalls,Columns,Spaces,Doors,Windows,Equipments,EquipParts,Ducts,Pipes,Connectors,BoundarySegments | 参见构件的定义 |
+  ------------ ---------------------------------------------------------------------------------------------------------------------------------------------- ------------------
+
+
+
+项目单位
+
+输入:
+
+输出:
+
+    {
+        "Id":"10005",
+        "ElementType":"Unit",
+        "SourceId":"",
+        "Name":"Length",
+        "Value":"mm",
+        "Tag":"长度单位"
+    }
+
+------------- ------------------------------ ----------
+|名称          |解释                           |示例|
+|---------|---------|---------------|
+|Id            |在提取过程中生成的Id           |10001|
+|ElementType   |构件类型,这里标识为标上单位   |Unit
+|SourceId      |对应BIM模型中的Id              |
+|Name          |单位名称                       |Length
+|Value         |单位的值                       |mm
+|Tag           |单位描述                       |长度单位
+------------- ------------------------------ ----------
+
+
+标高
+
+输入:
+
+输出:
+
+    {
+        "Id":"10005",
+        "ElementType":"Level",
+        "SourceId":"311",
+        "Name":"标高 1",
+        "Value":0,
+    }
+
+
+------------- -------------------------- --------
+|名称          |解释                       |示例|
+|-----          |----                       |-----|
+|Id            |在提取过程中生成的Id       |10001|
+|ElementType   |构件类型,这里标识为标高   |Level|
+|SourceId      |对应BIM模型中的Id         | 311|
+|Name          |标高名称                  | 标高 1|
+|Value         |标高的高度                 |0|
+------------- -------------------------- --------
+
+
+轴网
+
+输入:
+
+输出:
+
+    {
+        "Location":{
+            "Type":"Line",
+            "Points":[
+                {
+                    "X":16770.65,
+                    "Y":7008.54,
+                    "Z":0
+                },
+                {
+                    "X":-14798.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+        ],
+        "SourceId":"359378",
+        "Name":"1",
+        "Id":"10067",
+        "ElementType":"Grid"
+    }
+
+
+
+------------- -------------------------- --------------------
+|名称          |解释                       |示例|
+|-----          |-----                       |-----|
+|Id            |在提取过程中生成的Id       |10001|
+|ElementType   |构件类型,这里标识为轴网   |Grid|
+|SourceId      |对应BIM模型中的Id          | |
+|Name          |轴网名称                   |A,B,C,...1,2,3,...|
+|Location      |定位信息                   |格式见定位信息|
+|Description   |描述                      | |
+------------- -------------------------- --------------------
+
+
+构件-墙
+
+注意:有门窗的墙,墙轮廓为底部,多个拆分后的轮廓
+
+    {
+        "Width":200,
+        "Location":{
+            "Type":"Line",
+            "Points":[
+                {
+                    "X":-2298.19,
+                    "Y":7008.54,
+                    "Z":0
+                },
+                {
+                    "X":618.94,
+                    "Y":-4691.46,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-2376.32,
+                    "Y":6908.54,
+                    "Z":0
+                },
+                {
+                    "X":521.91,
+                    "Y":-4715.65,
+                    "Z":0
+                },
+                {
+                    "X":715.97,
+                    "Y":-4667.27,
+                    "Z":0
+                },
+                {
+                    "X":-2170.2,
+                    "Y":6908.54,
+                    "Z":0
+                },
+                {
+                    "X":-2376.32,
+                    "Y":6908.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "SourceId":"313107",
+        "Name":"常规- 200mm",
+        "Id":"10005",
+        "ElementType":"Wall"
+    }
+
+
+------------- -------------------------- -------------------------------------------
+|名称          |解释                       |示例|
+|---          |---                       |---|
+|Id            |在提取过程中生成的Id       |10009|
+|ElementType   |构件类型,这里标识为墙     |Wall|
+|SourceId      |对应BIM模型中的Id          |1159597|
+|Name          |构件名称                   ||
+|Location      |定位信息                   |线定位信息|
+|OutLine       |轮廓信息;墙的顶面轮廓线   |[[{"X":-2376.32,"Y":6908.54,"Z":0},{"X":521.91,"Y":-4715.65,"Z":0}]]数组的数组|
+|Width         |墙宽                       |120|
+------------- -------------------------- -------------------------------------------
+
+
+
+构件-虚拟墙
+
+    {
+        "Id":"10009",
+        "ElementType":"VirtualWall",
+        "SourceId":"1159597",
+        "Name":"",
+        "Location":{
+            "Type":"Line",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ]
+    }
+
+
+
+------------- ---------------------------------- ---------------------------------------
+|名称          |解释                               |示例|
+|---          |---                               |---|
+|Id            |在提取过程中生成的Id               |10009|
+|ElementType   |构件类型,这里标识为虚拟墙          |VirtualWall|
+|SourceId      |对应BIM模型中的Id                  |1159597|
+|Name          |构件名称                          | |
+|Location      |定位信息                          | 线定位信息|
+|OutLine       |轮廓信息;和定位信息中点描述相同   |[["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]]数组的数组|
+------------- ---------------------------------- ---------------------------------------
+
+
+构件-门
+
+    {
+        "Id":"10009",
+        "ElementType":"Door",
+        "SourceId":"1159597",
+        "Name":"双开门",
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":5026.81,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":4650.12,
+                    "Y":7008.54,
+                    "Z":0
+                },
+                {
+                    "X":5401.81,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "Owner":"10008",
+        "FaceDirection":"0,1,0",
+        "HandDirection":"1,0,0",
+        "Thick":120
+    }
+
+--------------- ------------------------ ------------------------------------------
+|名称            |解释                     |示例|
+|---            |---                     |---|
+|Id              |在提取过程中生成的Id     |10009|
+|ElementType     |构件类型,这里标识为门   |Door|
+|SourceId        |对应BIM模型中的Id        |1159597|
+|Name            |构件名称                || 
+|Location        |定位信息                 |点定位|
+|OutLine         |轮廓信息;一段直线段     |[[{"X":4650.12,"Y":7008.54,"Z":0},{"X":5401.81,"Y":7008.54,"Z":0}]]数组的数组|
+|Owner           |所属主体(墙),引用id   |10008|
+|FaceDirection   |门开口方向                |"0,1,0" 方向向量|
+|HandDirection   |                         |"1,0,0" 方向向量|
+|Thick           |厚度                     |120|
+--------------- ------------------------ ------------------------------------------
+
+
+
+
+
+
+构件-窗
+
+      
+    {
+        "Id":"10009",
+        "ElementType":"Window",
+        "SourceId":"1159597",
+        "Name":"窗",
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":4650.12,
+                    "Y":7008.54,
+                    "Z":0
+                },
+                {
+                    "X":5401.81,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "Owner":"10008",
+        "Thick":120
+    }
+
+
+
+
+------------- ------------------------ ------------------------------------------
+|名称          |解释                     |示例|
+|---          |---                     |---|
+|Id            |在提取过程中生成的Id     |10009|
+|ElementType   |构件类型,这里标识为窗   |Window|
+|SourceId      |对应BIM模型中的Id        |1159597
+|Name          |构件名称                | 
+|Location      |定位信息                 |点定位|
+|OutLine       |轮廓信息;一段直线段     |[[{"X":4650.12,"Y":7008.54,"Z":0},{"X":5401.81,"Y":7008.54,"Z":0}]]数组的数组|
+|Owner         |所属主体(墙),引用id   |10008|
+|Thick         |厚度                     |120|
+------------- ------------------------ ------------------------------------------
+
+
+
+
+构件-柱子
+
+    {
+        "Id":"10009",
+        "ElementType":"Column",
+        "SourceId":"1159597",
+        "Name":"200×200",
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+      "RoomBoundary":true,
+    }
+
+
+
+
+
+-------------- -------------------------- -------------------------------------------
+|名称           |解释                       |示例|
+|---           |----                       |----|
+|Id             |在提取过程中生成的Id       |10009|
+|ElementType    |构件类型,这里标识为柱子   |Column|
+|SourceId       |对应BIM模型中的Id          |1159597|
+|Name           |构件名称                   ||
+|Location       |定位信息                   |点定位信息:{"Type":"Point","Points":[{"X":-5498.19,"Y":7008.54,"Z":0}]}|
+|OutLine        |轮廓信息。模型顶面外轮廓   |[["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]]数组的数组|
+|RoomBoundary   |房间边界布尔类型           |true|
+-------------- -------------------------- -------------------------------------------
+
+
+构件-空间
+
+输入:Revit模型文件地址 \F1.rvt
+
+输出:
+
+    {
+        "Id":"10001",
+        "ElementType":"Space",
+        "SourceId":"316306",
+        "Name":"空间 1",
+        "Tag":"这里是一些补充信息",
+        "Parameters":[
+            {
+                "Name":"编号",
+                "Value":"1",
+            },
+            {
+                "Name":"名称",
+                "Value":"空间",
+            }
+        ],
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                "13.9579280905739, 26.0937686739499, 0"
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ],
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "BoundarySegments":[
+            [
+                "10001",
+                "10002",
+                "10003",
+                "10004"
+            ]
+        ]
+    }
+
+
+------------------ ------------------------------ --------------------------------------------
+|名称               |解释                           |示例|
+|----               |----                           |----|
+|Id                 |在提取过程中生成的Id           |10001|
+|ElementType        |构件类型,这里标识为一个空间   |Space|
+|SourceId           |对应BIM模型中的Id              |316306|
+|Name               |构件名称                       |空间 1|
+|Tag                |这里是一些补充信息            | |
+|Parameters         |从BIM模型中读取到的参数        |名称、编号|
+|Location           |定位信息                       |一般是定位点,也可以是定位线|
+|BoundarySegments   |关联轮廓信息                   |关联轮廓信息对应的id数组,数组的数组|
+|OutLine            |轮廓线                         |围成几何体的线,外圈是逆时针,内圈为顺时针|
+------------------ ------------------------------ --------------------------------------------
+
+
+
+构件-设备
+
+
+    {
+        "Id":"10002",
+        "ElementType":"Equipment",
+        "SourceId":"320720",
+        "Name":"CCOP-供冷冷却水泵",
+        "Tag":"这里是一些补充信息,例如待建模传入的岗位Id",
+        "FamilyName":"CCOP-供冷冷却水泵",
+        "Parameters":[
+            {
+                "Name":"设备本地名称",
+                "Value":"",
+            },
+            {
+                "Name":"设备本地编码",
+                "Value":"",
+            }
+        ],
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ]
+    }
+    
+------------- ------------------------------------------------ --------------------------------------------
+|名称          |解释                                             |示例|
+|----          |----                                             |----|
+|Id            |在提取过程中生成的Id                             |10001|
+|ElementType   |构件类型,这里标识为一个空间                     |Equipment|
+|SourceId      |对应BIM模型中的Id                                |316306|
+|Name          |构件名称,可能与族名称一样,但不等价于族名称,   |CCOP-供冷冷却水泵|
+|Tag           |这里是一些补充信息                              || 
+|FamilyName    |设备的族名称                                     |CCOP-供冷冷却水泵|
+|Parameters    |从BIM模型中读取到的参数                          |设备本地名称、设备本地编码编码|
+|Location      |定位信息                                        | 一般是定位点,也可以是定位线|
+|OutLine       |轮廓线                                          | 围成几何体的线,外圈是逆时针,内圈为顺时针|
+------------- ------------------------------------------------ --------------------------------------------
+
+
+构件-部件
+
+    {
+        "Id":"10001",
+        "ElementType":"EquipPart",
+        "SourceId":"333940",
+        "Name":"配电抽屉320*800",
+        "Tag":"这里是一些补充信息,例如待建模传入的岗位Id",
+        "FamilyName":"TDLSDW-低压配电抽屉",
+        "Owner":"10002",
+        "Parameters":[
+            {
+                "Name":"设备本地名称",
+                "Value":"",
+            },
+            {
+                "Name":"设备本地编码",
+                "Value":"",
+            }
+        ],
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ]
+    }
+
+
+------------- ----------------------------------------------------- --------------------------------------------
+|名称          |解释                                                  |示例|
+|----          |-----                                                  |----|
+|Id            |在提取过程中生成的Id                                  |10001|
+|ElementType   |构件类型,这里标识为一个空间                          |EquipPart|
+|SourceId      |对应BIM模型中的Id                                     |316306|
+|Name          |构件名称,可能与族名称一样,但不等价于族名称,       | 配电抽屉320*800|
+|Tag           |这里是一些补充信息                                  ||  
+|FamilyName    |设备的族名称                                          |TDLSDW-低压配电抽屉|
+|Owner         |关系-所属设备的Id,这里是引用的Id,不是BIM模型中的Id   |10002|
+|Parameters    |从BIM模型中读取到的参数                               |设备本地名称、设备本地编码编码|
+|Location      |定位信息                                              |一般是定位点,也可以是定位线|
+|OutLine       |轮廓线                                               | 围成几何体的线,外圈是逆时针,内圈为顺时针|
+------------- ----------------------------------------------------- --------------------------------------------
+
+构件-信标
+
+暂时不清楚
+
+构件-水管      
+
+    {
+        "Id":"10009",
+        "ElementType":"Pipe",
+        "SourceId":"1159597",
+        "Name":"铝塑复合管-卡套",
+        "Location":{
+            "Type":"Line",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "MepSystemTypeName":"中水系统",
+        "Diameter":80,
+        "ConnectedIds":[
+            "10010",
+            "10011"
+        ]
+    }
+
+
+------------------- -------------------------------------- --------------------------------------------
+|名称                |解释                                   |示例|
+|----                |----                                   |----|
+|Id                  |在提取过程中生成的Id                   |10009|
+|ElementType         |构件类型,这里标识为水管               |Pipe|
+|SourceId            |对应BIM模型中的Id                      |1159597|
+|Name                |构件名称                               ||
+|Location            |定位信息                               |是一个直线段:{"Type":"Line","Points":["-1997.74,-17554.18,1128","29909.39,-17554.18,1128"]}|
+|OutLine             |轮廓信息                               |[["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]]数组的数组|
+|MepSystemTypeName   |管道系统类型名称                       |中水系统|
+|Diameter            |直径                                   |80|
+|ConnectedIds        |包含的连接点信息。连接点id信息的数组   |["10016"]|
+------------------- -------------------------------------- --------------------------------------------
+
+
+构件-风管
+
+    {
+        "Id":"10009",
+        "ElementType":"Duct",
+        "SourceId":"1159597",
+        "Name":"铝材",
+        "Location":{
+            "Type":"Line",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "MepSystemTypeName":"中水系统",
+        "Shape":"Circle|Rectangle|Ellipse",
+        "Diameter":13.34,
+        "Width":200,
+        "Height":100,
+        "ConnectedIds":[
+            "10010",
+            "10011"
+        ]
+    }
+
+
+
+------------------- -------------------------------------- --------------------------------------------
+|名称                |解释                                   |示例|
+|----                |----                                   |----|
+|Id                  |在提取过程中生成的Id                   |10009|
+|ElementType         |构件类型,这里标识为风管               |Duct|
+|SourceId            |对应BIM模型中的Id                      |1159597|
+|Name                |构件名称                              || 
+|Location            |道定位信息                             |是一个直线段:{"Type":"Line","Points":["-1997.74,-17554.18,1128","29909.39,-17554.18,1128"]}|
+|OutLine             |轮廓信息                               |[["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]]数组的数组|
+|MepSystemTypeName   |管道系统类型名称                       |送风|
+|Shape               |风管形状;枚举类型                     |Circle or Rectangle or Ellipse|
+|Diameter            |直径径;当形状为Circle时有效           |80|
+|Width               |宽度;当形状为Rectangle或者Ellipse     |300|
+|Height              |高度;当形状为Rectangle或者Ellipse     |100|
+|ConnectedIds        |包含的连接点信息。连接点id信息的数组    |["10016","10017"]|
+------------------- -------------------------------------- --------------------------------------------
+
+
+
+
+其他构件(有Connector的,管道和设备以外的构件)
+
+    {
+        "Id":"10009",
+        "ElementType":"Other",
+        "SourceId":"1159597",
+        "Name":"标准",
+        "Location":{
+            "Type":"Point",
+            "Points":[
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        },
+        "OutLine":[
+            [
+                {
+                    "X":-5498.19,
+                    "Y":7008.54,
+                    "Z":0
+                }
+            ]
+        ],
+        "ConnectedIds":[
+            "10010",
+            "10011"
+        ]
+    }
+
+
+
+-------------- -------------------------------------- ---------------------------------------
+|名称           |解释                                   |示例|
+|----     |----                                         |----|
+|Id             |在提取过程中生成的Id                   |10009|
+|ElementType    |构件类型,这里标识其他构件             |Other|
+|SourceId       |对应BIM模型中的Id                      |1159597|
+|Name           |构件名称                               ||
+|Location       |定位信息                               |点定位信息和线定位信息都有可能存在|
+|OutLine        |轮廓信息                               |[["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]]数组的数组|
+|ConnectedIds   |包含的连接点信息。连接点id信息的数组    |["10016"]|
+-------------- -------------------------------------- ---------------------------------------
+
+
+连接点 connecor
+
+    {
+        "Id":"10010",
+        "ElementType":"Connector",
+        "SourceId":"1159597_C_0",
+        "Domain":"DomainPiping",
+        "MepSystemTypeName":"中水系统",
+        "IsConnected":true,
+        "Origin":{
+            "X":-5498.19,
+            "Y":7008.54,
+            "Z":0
+        },
+        "Owner":"10009",
+        "ConnectedIds":[
+            "10016"
+        ]
+    }
+
+Plain Text
+
+Domain的枚举值:
+
+DomainUndefined Domain is undefined  
+
+DomainHvac HVAC (duct) connector  
+
+DomainElectrical Electrical connector  
+
+DomainPiping Connector of cable trays or conduits  
+
+DomainCableTrayConduit Connector of Structural Analytical Model  
+
+
+
+------------------- ---------------------------------------------- -------------------------------
+|名称                |解释                                           |示例|
+|----     |----                                         |----|
+|Id                  |在提取过程中生成的Id                           |10001|
+|ElementType         |构件类型,这里标识为连接件                     |Connector
+|SourceId            |对应BIM模型中的Id                              |316306|
+|Domain              |连接点所属专业类别                             |DomainPiping|
+|Description         |连接点描述信息                                 ||
+|MepSystemTypeName   |连接点所属管网的系统类型名称                   |中水系统|
+|IsConnected         |标志是否处于连接状态                           |类型为布尔类型|
+|Origin              |连接点定位信息                                 |XYZ形式坐标|
+|Owner               |连接点所关联的父对象引用Id                     |10009;关联id为10009的实体对象|
+|ConnectedIds        |和连接点存在连接关系的其他连接点的id信息数组    |["10016"]|
+------------------- ---------------------------------------------- -------------------------------
+
+
+空间轮廓线段
+
+    {
+        "Id":"10009",
+        "ElementType":"BoundarySegment",
+        "Reference":"10556",
+        "Curve":[
+            {
+                "X":-5498.19,
+                "Y":7008.54,
+                "Z":0
+            }
+        ]
+    }
+
+
+
+------------- --------------------------------------------------------------------- ---------------------------------------
+|名称          |解释                                                                  |示例|
+|----     |----                                         |----|
+|Id            |在提取过程中生成的Id                                                  |10009|
+|ElementType   |构件类型,这里标识为 空间轮廓线段                                     |BoundarySegment|
+|Reference     |线段关联实体构件Id,可以为空, 指向墙id, 虚拟墙id, 柱子id(可能门窗id)    |10556|
+|Curve         |线,点的数组                                                         |["-2090.61,10352.94,1128","-2090.61,-17461.31,1128"]|
+------------- --------------------------------------------------------------------- ---------------------------------------
+
+
+定位信息
+
+输入:
+
+输出:
+
+    {
+        "Type":"Line",
+        "Points":[
+            {
+                "X":-5498.19,
+                "Y":7008.54,
+                "Z":0
+            },
+            {
+                "X":12101.81,
+                "Y":7008.54,
+                "Z":0
+            }
+        ]
+    }
+
+-------- -------------------------------------------- -------------
+|名称     |解释                                         |示例|
+|----     |----                                         |----|
+|Type     |定位信息的类型                               |Point,Line|
+|Points   |定位点的集合,如果为弧被拆分成多段线表示。   ||
+-------- -------------------------------------------- -------------
+
+
+
+总体返回对象信息
+
+地址:[http://git.sagacloud.cn/revit/RevitExport/src/4246dff16d3268ae165222687fa6b8a70779d4f0/20190628114542.json](http://git.sagacloud.cn/revit/RevitExport/src/4246dff16d3268ae165222687fa6b8a70779d4f0/20190628114542.json)

+ 11 - 0
docs/setup/centos/app-server/revit-service.md

@@ -0,0 +1,11 @@
+# revit服务框架
+
+## 技术细节
+    
+    1. DotNetty进行网络传输
+    2. protobuf进行通信编码解码
+    3. NHibernate做(mysql)数据持久层
+    4. 消息驱动
+    
+    
+    

+ 36 - 0
docs/setup/centos/app-server/scheduler.md

@@ -0,0 +1,36 @@
+# 模型文件调度器
+
+## 项目结构
+![项目结构](./img/scheduler_001.jpg)
+
+    如图中所标记
+    1. protobuf自动生成类
+    2. 对外服务接口
+    3. 实体类
+    4. TCP服务器端
+    5. 所有的服务类
+    6. 常用的服务
+    7. protobuf定义文件
+    8. 配置文件
+    9. 测试类
+    10. Gradle的build文件
+    
+## 项目介绍
+
+### 功能
+    1. 提供接受处理模型的任务, 接受模型客户端连接, 并根据roundRobbin算法调度任务给各个客户端处理服务
+    2. 提供检索任务状态, 任务处理结果服务
+    3. 提供给客户端提交任务处理结果和状态的服务
+    
+### 技术细节
+    1. 跟客户端的连接和通信使用netty(tcp)和protobuf编码解码
+    2. 发送JMS消息使用activemq
+    
+## 打包
+
+    如果打测试包, 在右侧Gradle的管理页面里双击任务buildDevJar或者buildDevWar即可. 打包内容中就会
+    把左侧资源文件包resources和resources-dev的内容加入到打包的资源内, 建议在打包前把上次打包的文件删除
+    
+
+## 服务依赖
+    无

BIN
docs/setup/centos/server/img/pgdocker_001.jpg


BIN
docs/setup/centos/server/img/pgdocker_002.png


BIN
docs/setup/centos/server/img/pgdocker_003.jpg


BIN
docs/setup/centos/server/img/pgdocker_004.jpg


+ 780 - 0
docs/setup/centos/server/postgresql-docker.md

@@ -0,0 +1,780 @@
+# centos7下使用docker安装postgresql服务(带plpython3u扩展)
+
+## 安装docker (使用root用户)
+
+    1. 更新yum
+        yum update
+    2. 安装必要的依赖
+        yum install -y yum-utils device-mapper-persistent-data lvm2
+    3. 设置yum源(二选一)
+        yum-config-manager --add-repo http://download.docker.com/linux/centos/docker-ce.repo(中央仓库)
+        yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo(阿里仓库)
+        
+    4. 查看仓库中docker版本
+        yum list docker-ce --showduplicates | sort -r
+    5. 安装docker
+        yum install docker-ce-版本号
+        (例如: yum install docker-ce-18.03.1.ce)
+    6. 启动docker并将docker加入开机启动
+        1). systemctl start docker
+        2). systemctl enable docker
+        
+    7. docker帮助
+        docker --help
+        docker 命令 --help
+
+## 创建镜像
+
+    1. 创建临时工作目录
+    
+        cd ~ && mkdir dockerTemp && cd dockerTemp
+
+    2. 拷贝docker-entrypoint.sh 和 Dockerfile进临时工作目录
+        (文件名大小写敏感, docker-entrypoint.sh 需要有可执行权限, chmod 777 docker-entrypoint.sh)
+    
+![pg001](./img/pgdocker_001.jpg)
+
+<details>
+<summary>docker-entrypoint.sh 内容</summary>
+
+    #!/usr/bin/env bash
+    set -Eeo pipefail
+    # TODO swap to -Eeuo pipefail above (after handling all potentially-unset variables)
+    
+    # usage: file_env VAR [DEFAULT]
+    #    ie: file_env 'XYZ_DB_PASSWORD' 'example'
+    # (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
+    #  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
+    file_env() {
+        local var="$1"
+        local fileVar="${var}_FILE"
+        local def="${2:-}"
+        if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
+            echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
+            exit 1
+        fi
+        local val="$def"
+        if [ "${!var:-}" ]; then
+            val="${!var}"
+        elif [ "${!fileVar:-}" ]; then
+            val="$(< "${!fileVar}")"
+        fi
+        export "$var"="$val"
+        unset "$fileVar"
+    }
+    
+    # check to see if this file is being run or sourced from another script
+    _is_sourced() {
+        # https://unix.stackexchange.com/a/215279
+        [ "${#FUNCNAME[@]}" -ge 2 ] \
+            && [ "${FUNCNAME[0]}" = '_is_sourced' ] \
+            && [ "${FUNCNAME[1]}" = 'source' ]
+    }
+    
+    # used to create initial postgres directories and if run as root, ensure ownership to the "postgres" user
+    docker_create_db_directories() {
+        local user; user="$(id -u)"
+    
+        mkdir -p "$PGDATA"
+        chmod 700 "$PGDATA"
+    
+        # ignore failure since it will be fine when using the image provided directory; see also https://github.com/docker-library/postgres/pull/289
+        mkdir -p /var/run/postgresql || :
+        chmod 775 /var/run/postgresql || :
+    
+        # Create the transaction log directory before initdb is run so the directory is owned by the correct user
+        if [ -n "$POSTGRES_INITDB_WALDIR" ]; then
+            mkdir -p "$POSTGRES_INITDB_WALDIR"
+            if [ "$user" = '0' ]; then
+                find "$POSTGRES_INITDB_WALDIR" \! -user postgres -exec chown postgres '{}' +
+            fi
+            chmod 700 "$POSTGRES_INITDB_WALDIR"
+        fi
+    
+        # allow the container to be started with `--user`
+        if [ "$user" = '0' ]; then
+            find "$PGDATA" \! -user postgres -exec chown postgres '{}' +
+            find /var/run/postgresql \! -user postgres -exec chown postgres '{}' +
+        fi
+    }
+    
+    # initialize empty PGDATA directory with new database via 'initdb'
+    # arguments to `initdb` can be passed via POSTGRES_INITDB_ARGS or as arguments to this function
+    # `initdb` automatically creates the "postgres", "template0", and "template1" dbnames
+    # this is also where the database user is created, specified by `POSTGRES_USER` env
+    docker_init_database_dir() {
+        # "initdb" is particular about the current user existing in "/etc/passwd", so we use "nss_wrapper" to fake that if necessary
+        # see https://github.com/docker-library/postgres/pull/253, https://github.com/docker-library/postgres/issues/359, https://cwrap.org/nss_wrapper.html
+        if ! getent passwd "$(id -u)" &> /dev/null && [ -e /usr/lib/libnss_wrapper.so ]; then
+            export LD_PRELOAD='/usr/lib/libnss_wrapper.so'
+            export NSS_WRAPPER_PASSWD="$(mktemp)"
+            export NSS_WRAPPER_GROUP="$(mktemp)"
+            echo "postgres:x:$(id -u):$(id -g):PostgreSQL:$PGDATA:/bin/false" > "$NSS_WRAPPER_PASSWD"
+            echo "postgres:x:$(id -g):" > "$NSS_WRAPPER_GROUP"
+        fi
+    
+        if [ -n "$POSTGRES_INITDB_WALDIR" ]; then
+            set -- --waldir "$POSTGRES_INITDB_WALDIR" "$@"
+        fi
+    
+        eval 'initdb --username="$POSTGRES_USER" --pwfile=<(echo "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"'
+    
+        # unset/cleanup "nss_wrapper" bits
+        if [ "${LD_PRELOAD:-}" = '/usr/lib/libnss_wrapper.so' ]; then
+            rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP"
+            unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP
+        fi
+    }
+    
+    # print large warning if POSTGRES_PASSWORD is long
+    # error if both POSTGRES_PASSWORD is empty and POSTGRES_HOST_AUTH_METHOD is not 'trust'
+    # print large warning if POSTGRES_HOST_AUTH_METHOD is set to 'trust'
+    # assumes database is not set up, ie: [ -z "$DATABASE_ALREADY_EXISTS" ]
+    docker_verify_minimum_env() {
+        # check password first so we can output the warning before postgres
+        # messes it up
+        if [ "${#POSTGRES_PASSWORD}" -ge 100 ]; then
+            cat >&2 <<-'EOWARN'
+    
+                WARNING: The supplied POSTGRES_PASSWORD is 100+ characters.
+    
+                  This will not work if used via PGPASSWORD with "psql".
+    
+                  https://www.postgresql.org/message-id/flat/E1Rqxp2-0004Qt-PL%40wrigleys.postgresql.org (BUG #6412)
+                  https://github.com/docker-library/postgres/issues/507
+    
+            EOWARN
+        fi
+        if [ -z "$POSTGRES_PASSWORD" ] && [ 'trust' != "$POSTGRES_HOST_AUTH_METHOD" ]; then
+            # The - option suppresses leading tabs but *not* spaces. :)
+            cat >&2 <<-'EOE'
+                Error: Database is uninitialized and superuser password is not specified.
+                       You must specify POSTGRES_PASSWORD to a non-empty value for the
+                       superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".
+    
+                       You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all
+                       connections without a password. This is *not* recommended.
+    
+                       See PostgreSQL documentation about "trust":
+                       https://www.postgresql.org/docs/current/auth-trust.html
+            EOE
+            exit 1
+        fi
+        if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then
+            cat >&2 <<-'EOWARN'
+                ********************************************************************************
+                WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow
+                         anyone with access to the Postgres port to access your database without
+                         a password, even if POSTGRES_PASSWORD is set. See PostgreSQL
+                         documentation about "trust":
+                         https://www.postgresql.org/docs/current/auth-trust.html
+                         In Docker's default configuration, this is effectively any other
+                         container on the same system.
+    
+                         It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace
+                         it with "-e POSTGRES_PASSWORD=password" instead to set a password in
+                         "docker run".
+                ********************************************************************************
+            EOWARN
+        fi
+    }
+    
+    # usage: docker_process_init_files [file [file [...]]]
+    #    ie: docker_process_init_files /always-initdb.d/*
+    # process initializer files, based on file extensions and permissions
+    docker_process_init_files() {
+        # psql here for backwards compatiblilty "${psql[@]}"
+        psql=( docker_process_sql )
+    
+        echo
+        local f
+        for f; do
+            case "$f" in
+                *.sh)
+                    # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936
+                    # https://github.com/docker-library/postgres/pull/452
+                    if [ -x "$f" ]; then
+                        echo "$0: running $f"
+                        "$f"
+                    else
+                        echo "$0: sourcing $f"
+                        . "$f"
+                    fi
+                    ;;
+                *.sql)    echo "$0: running $f"; docker_process_sql -f "$f"; echo ;;
+                *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | docker_process_sql; echo ;;
+                *.sql.xz) echo "$0: running $f"; xzcat "$f" | docker_process_sql; echo ;;
+                *)        echo "$0: ignoring $f" ;;
+            esac
+            echo
+        done
+    }
+    
+    # Execute sql script, passed via stdin (or -f flag of pqsl)
+    # usage: docker_process_sql [psql-cli-args]
+    #    ie: docker_process_sql --dbname=mydb <<<'INSERT ...'
+    #    ie: docker_process_sql -f my-file.sql
+    #    ie: docker_process_sql <my-file.sql
+    docker_process_sql() {
+        local query_runner=( psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --no-password )
+        if [ -n "$POSTGRES_DB" ]; then
+            query_runner+=( --dbname "$POSTGRES_DB" )
+        fi
+    
+        "${query_runner[@]}" "$@"
+    }
+    
+    # create initial database
+    # uses environment variables for input: POSTGRES_DB
+    docker_setup_db() {
+        if [ "$POSTGRES_DB" != 'postgres' ]; then
+            POSTGRES_DB= docker_process_sql --dbname postgres --set db="$POSTGRES_DB" <<-'EOSQL'
+                CREATE DATABASE :"db" ;
+            EOSQL
+            echo
+        fi
+    }
+    
+    # Loads various settings that are used elsewhere in the script
+    # This should be called before any other functions
+    docker_setup_env() {
+        file_env 'POSTGRES_PASSWORD'
+    
+        file_env 'POSTGRES_USER' 'postgres'
+        file_env 'POSTGRES_DB' "$POSTGRES_USER"
+        file_env 'POSTGRES_INITDB_ARGS'
+        # default authentication method is md5
+        : "${POSTGRES_HOST_AUTH_METHOD:=md5}"
+    
+        declare -g DATABASE_ALREADY_EXISTS
+        # look specifically for PG_VERSION, as it is expected in the DB dir
+        if [ -s "$PGDATA/PG_VERSION" ]; then
+            DATABASE_ALREADY_EXISTS='true'
+        fi
+    }
+    
+    # append POSTGRES_HOST_AUTH_METHOD to pg_hba.conf for "host" connections
+    pg_setup_hba_conf() {
+        {
+            echo
+            if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then
+                echo '# warning trust is enabled for all connections'
+                echo '# see https://www.postgresql.org/docs/12/auth-trust.html'
+            fi
+            echo "host all all all $POSTGRES_HOST_AUTH_METHOD"
+        } >> "$PGDATA/pg_hba.conf"
+    }
+    
+    # start socket-only postgresql server for setting up or running scripts
+    # all arguments will be passed along as arguments to `postgres` (via pg_ctl)
+    docker_temp_server_start() {
+        if [ "$1" = 'postgres' ]; then
+            shift
+        fi
+    
+        # internal start of server in order to allow setup using psql client
+        # does not listen on external TCP/IP and waits until start finishes
+        set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}"
+    
+        PGUSER="${PGUSER:-$POSTGRES_USER}" \
+        pg_ctl -D "$PGDATA" \
+            -o "$(printf '%q ' "$@")" \
+            -w start
+    }
+    
+    # stop postgresql server after done setting up user and running scripts
+    docker_temp_server_stop() {
+        PGUSER="${PGUSER:-postgres}" \
+        pg_ctl -D "$PGDATA" -m fast -w stop
+    }
+    
+    # check arguments for an option that would cause postgres to stop
+    # return true if there is one
+    _pg_want_help() {
+        local arg
+        for arg; do
+            case "$arg" in
+                # postgres --help | grep 'then exit'
+                # leaving out -C on purpose since it always fails and is unhelpful:
+                # postgres: could not access the server configuration file "/var/lib/postgresql/data/postgresql.conf": No such file or directory
+                -'?'|--help|--describe-config|-V|--version)
+                    return 0
+                    ;;
+            esac
+        done
+        return 1
+    }
+    
+    _main() {
+        # if first arg looks like a flag, assume we want to run postgres server
+        if [ "${1:0:1}" = '-' ]; then
+            set -- postgres "$@"
+        fi
+    
+        if [ "$1" = 'postgres' ] && ! _pg_want_help "$@"; then
+            docker_setup_env
+            # setup data directories and permissions (when run as root)
+            docker_create_db_directories
+            if [ "$(id -u)" = '0' ]; then
+                # then restart script as postgres user
+                exec gosu postgres "$BASH_SOURCE" "$@"
+            fi
+    
+            # only run initialization on an empty data directory
+            if [ -z "$DATABASE_ALREADY_EXISTS" ]; then
+                docker_verify_minimum_env
+    
+                # check dir permissions to reduce likelihood of half-initialized database
+                ls /docker-entrypoint-initdb.d/ > /dev/null
+    
+                docker_init_database_dir
+                pg_setup_hba_conf
+    
+                # PGPASSWORD is required for psql when authentication is required for 'local' connections via pg_hba.conf and is otherwise harmless
+                # e.g. when '--auth=md5' or '--auth-local=md5' is used in POSTGRES_INITDB_ARGS
+                export PGPASSWORD="${PGPASSWORD:-$POSTGRES_PASSWORD}"
+                docker_temp_server_start "$@"
+    
+                docker_setup_db
+                docker_process_init_files /docker-entrypoint-initdb.d/*
+    
+                docker_temp_server_stop
+                unset PGPASSWORD
+    
+                echo
+                echo 'PostgreSQL init process complete; ready for start up.'
+                echo
+            else
+                echo
+                echo 'PostgreSQL Database directory appears to contain a database; Skipping initialization'
+                echo
+            fi
+        fi
+    
+        exec "$@"
+    }
+    
+    if ! _is_sourced; then
+        _main "$@"
+    fi
+
+</details>
+
+<details>
+<summary>Dockerfile 内容</summary>
+
+    # FROM debian:buster-slim AS BASE1
+    
+    
+    
+    FROM buildpack-deps:buster
+    
+    # ensure local python is preferred over distribution python
+    ENV PATH /usr/local/bin:$PATH
+    
+    # http://bugs.python.org/issue19846
+    # > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
+    ENV LANG C.UTF-8
+    
+    # extra dependencies (over what buildpack-deps already includes)
+    RUN apt-get update && apt-get install -y --no-install-recommends \
+    		libbluetooth-dev \
+    		tk-dev \
+    		uuid-dev \
+    	&& rm -rf /var/lib/apt/lists/*
+    
+    ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
+    ENV PYTHON_VERSION 3.7.7
+    
+    RUN set -ex \
+    	\
+    	&& wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \
+    	&& wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \
+    	&& export GNUPGHOME="$(mktemp -d)" \
+    	&& gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" \
+    	&& gpg --batch --verify python.tar.xz.asc python.tar.xz \
+    	&& { command -v gpgconf > /dev/null && gpgconf --kill all || :; } \
+    	&& rm -rf "$GNUPGHOME" python.tar.xz.asc \
+    	&& mkdir -p /usr/src/python \
+    	&& tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz \
+    	&& rm python.tar.xz \
+    	\
+    	&& cd /usr/src/python \
+    	&& gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
+    	&& ./configure \
+    		--build="$gnuArch" \
+    		--enable-loadable-sqlite-extensions \
+    		--enable-optimizations \
+    		--enable-option-checking=fatal \
+    		--enable-shared \
+    		--with-system-expat \
+    		--with-system-ffi \
+    		--without-ensurepip \
+    	&& make -j "$(nproc)" \
+    # setting PROFILE_TASK makes "--enable-optimizations" reasonable: https://bugs.python.org/issue36044 / https://github.com/docker-library/python/issues/160#issuecomment-509426916
+    		PROFILE_TASK='-m test.regrtest --pgo \
+    			test_array \
+    			test_base64 \
+    			test_binascii \
+    			test_binhex \
+    			test_binop \
+    			test_bytes \
+    			test_c_locale_coercion \
+    			test_class \
+    			test_cmath \
+    			test_codecs \
+    			test_compile \
+    			test_complex \
+    			test_csv \
+    			test_decimal \
+    			test_dict \
+    			test_float \
+    			test_fstring \
+    			test_hashlib \
+    			test_io \
+    			test_iter \
+    			test_json \
+    			test_long \
+    			test_math \
+    			test_memoryview \
+    			test_pickle \
+    			test_re \
+    			test_set \
+    			test_slice \
+    			test_struct \
+    			test_threading \
+    			test_time \
+    			test_traceback \
+    			test_unicode \
+    		' \
+    	&& make install \
+    	&& ldconfig \
+    	\
+    	&& find /usr/local -depth \
+    		\( \
+    			\( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
+    			-o \
+    			\( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
+    		\) -exec rm -rf '{}' + \
+    	&& rm -rf /usr/src/python \
+    	\
+    	&& python3 --version
+    
+    # make some useful symlinks that are expected to exist
+    RUN cd /usr/local/bin \
+    	&& ln -s idle3 idle \
+    	&& ln -s pydoc3 pydoc \
+    	&& ln -s python3 python \
+    	&& ln -s python3-config python-config
+    
+    # if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
+    ENV PYTHON_PIP_VERSION 20.1.1
+    # https://github.com/pypa/get-pip
+    ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/eff16c878c7fd6b688b9b4c4267695cf1a0bf01b/get-pip.py
+    ENV PYTHON_GET_PIP_SHA256 b3153ec0cf7b7bbf9556932aa37e4981c35dc2a2c501d70d91d2795aa532be79
+    
+    RUN set -ex; \
+    	\
+    	wget -O get-pip.py "$PYTHON_GET_PIP_URL"; \
+    	echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum --check --strict -; \
+    	\
+    	python get-pip.py \
+    		--disable-pip-version-check \
+    		--no-cache-dir \
+    		"pip==$PYTHON_PIP_VERSION" \
+    	; \
+    	pip --version; \
+    	\
+    	find /usr/local -depth \
+    		\( \
+    			\( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
+    			-o \
+    			\( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
+    		\) -exec rm -rf '{}' +; \
+    	rm -f get-pip.py
+    
+    RUN pip3 install shapely -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& pip3 install matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& pip3 install short-text-sim -i http://dev.dp.sagacloud.cn:8082/repository/saga-pypi-group/simple --trusted-host dev.dp.sagacloud.cn \
+    	&& pip3 install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& pip3 install vg -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& pip3 install networkx -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& pip3 install jieba -i https://pypi.tuna.tsinghua.edu.cn/simple \
+    	&& cd /usr/local/lib/python3.7/site-packages \
+    	&& git clone http://39.106.8.246:3003/chenhaiyang/relations.git
+    
+    
+    # CMD ["python3"]
+    
+    
+    # FROM debian:buster-slim
+    
+    RUN set -ex; \
+    	if ! command -v gpg > /dev/null; then \
+    		apt-get update; \
+    		apt-get install -y --no-install-recommends \
+    			gnupg \
+    			dirmngr \
+    		; \
+    		rm -rf /var/lib/apt/lists/*; \
+    	fi
+    
+    # explicitly set user/group IDs
+    RUN set -eux; \
+    	groupadd -r postgres --gid=999; \
+    # https://salsa.debian.org/postgresql/postgresql-common/blob/997d842ee744687d99a2b2d95c1083a2615c79e8/debian/postgresql-common.postinst#L32-35
+    	useradd -r -g postgres --uid=999 --home-dir=/var/lib/postgresql --shell=/bin/bash postgres; \
+    # also create the postgres user's home directory with appropriate permissions
+    # see https://github.com/docker-library/postgres/issues/274
+    	mkdir -p /var/lib/postgresql; \
+    	chown -R postgres:postgres /var/lib/postgresql
+    
+    # grab gosu for easy step-down from root
+    # https://github.com/tianon/gosu/releases
+    ENV GOSU_VERSION 1.12
+    RUN set -eux; \
+    	savedAptMark="$(apt-mark showmanual)"; \
+    	apt-get update; \
+    	apt-get install -y --no-install-recommends ca-certificates wget; \
+    	rm -rf /var/lib/apt/lists/*; \
+    	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
+    	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
+    	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
+    	export GNUPGHOME="$(mktemp -d)"; \
+    	gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
+    	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
+    	gpgconf --kill all; \
+    	rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
+    	apt-mark auto '.*' > /dev/null; \
+    	[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
+    	apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
+    	chmod +x /usr/local/bin/gosu; \
+    	gosu --version; \
+    	gosu nobody true
+    
+    # make the "en_US.UTF-8" locale so postgres will be utf-8 enabled by default
+    RUN set -eux; \
+    	if [ -f /etc/dpkg/dpkg.cfg.d/docker ]; then \
+    # if this file exists, we're likely in "debian:xxx-slim", and locales are thus being excluded so we need to remove that exclusion (since we need locales)
+    		grep -q '/usr/share/locale' /etc/dpkg/dpkg.cfg.d/docker; \
+    		sed -ri '/\/usr\/share\/locale/d' /etc/dpkg/dpkg.cfg.d/docker; \
+    		! grep -q '/usr/share/locale' /etc/dpkg/dpkg.cfg.d/docker; \
+    	fi; \
+    	apt-get update; apt-get install -y --no-install-recommends locales; rm -rf /var/lib/apt/lists/*; \
+    	localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
+    ENV LANG en_US.utf8
+    
+    RUN set -eux; \
+    	apt-get update; \
+    	apt-get install -y --no-install-recommends \
+    # install "nss_wrapper" in case we need to fake "/etc/passwd" and "/etc/group" (especially for OpenShift)
+    # https://github.com/docker-library/postgres/issues/359
+    # https://cwrap.org/nss_wrapper.html
+    		libnss-wrapper \
+    # install "xz-utils" for .sql.xz docker-entrypoint-initdb.d files
+    		xz-utils \
+    	; \
+    	rm -rf /var/lib/apt/lists/*
+    
+    RUN mkdir /docker-entrypoint-initdb.d
+    
+    RUN set -ex; \
+    # pub   4096R/ACCC4CF8 2011-10-13 [expires: 2019-07-02]
+    #       Key fingerprint = B97B 0AFC AA1A 47F0 44F2  44A0 7FCC 7D46 ACCC 4CF8
+    # uid                  PostgreSQL Debian Repository
+    	key='B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8'; \
+    	export GNUPGHOME="$(mktemp -d)"; \
+    	gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \
+    	gpg --batch --export "$key" > /etc/apt/trusted.gpg.d/postgres.gpg; \
+    	command -v gpgconf > /dev/null && gpgconf --kill all; \
+    	rm -rf "$GNUPGHOME"; \
+    	apt-key list
+    
+    ENV PG_MAJOR 12
+    ENV PG_VERSION 12.3-1.pgdg100+1
+    
+    RUN set -ex; \
+    	\
+    # see note below about "*.pyc" files
+    	export PYTHONDONTWRITEBYTECODE=1; \
+    	\
+    	dpkgArch="$(dpkg --print-architecture)"; \
+    	case "$dpkgArch" in \
+    		amd64 | arm64 | i386 | ppc64el) \
+    # arches officialy built by upstream
+    			echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main $PG_MAJOR" > /etc/apt/sources.list.d/pgdg.list; \
+    			apt-get update; \
+    			;; \
+    		*) \
+    # we're on an architecture upstream doesn't officially build for
+    # let's build binaries from their published source packages
+    			echo "deb-src http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main $PG_MAJOR" > /etc/apt/sources.list.d/pgdg.list; \
+    			\
+    			case "$PG_MAJOR" in \
+    				9.* | 10 ) ;; \
+    				*) \
+    # https://github.com/docker-library/postgres/issues/484 (clang-6.0 required, only available in stretch-backports)
+    # TODO remove this once we hit buster+
+    					echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list.d/pgdg.list; \
+    					;; \
+    			esac; \
+    			\
+    			tempDir="$(mktemp -d)"; \
+    			cd "$tempDir"; \
+    			\
+    			savedAptMark="$(apt-mark showmanual)"; \
+    			\
+    # build .deb files from upstream's source packages (which are verified by apt-get)
+    			apt-get update; \
+    			apt-get build-dep -y \
+    				postgresql-common pgdg-keyring \
+    				"postgresql-$PG_MAJOR=$PG_VERSION" \
+    			; \
+    			DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
+    				apt-get source --compile \
+    					postgresql-common pgdg-keyring \
+    					"postgresql-$PG_MAJOR=$PG_VERSION" \
+    			; \
+    # we don't remove APT lists here because they get re-downloaded and removed later
+    			\
+    # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
+    # (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
+    			apt-mark showmanual | xargs apt-mark auto > /dev/null; \
+    			apt-mark manual $savedAptMark; \
+    			\
+    # create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
+    			ls -lAFh; \
+    			dpkg-scanpackages . > Packages; \
+    			grep '^Package: ' Packages; \
+    			echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list; \
+    # work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
+    #   Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
+    #   ...
+    #   E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages  Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
+    			apt-get -o Acquire::GzipIndexes=false update; \
+    			;; \
+    	esac; \
+    	\
+    	apt-get install -y --no-install-recommends postgresql-common; \
+    	sed -ri 's/#(create_main_cluster) .*$/\1 = false/' /etc/postgresql-common/createcluster.conf; \
+    	apt-get install -y --no-install-recommends \
+    		"postgresql-$PG_MAJOR=$PG_VERSION" \
+    	; \
+    	\
+    	rm -rf /var/lib/apt/lists/*; \
+    	\
+    	if [ -n "$tempDir" ]; then \
+    # if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
+    		apt-get purge -y --auto-remove; \
+    		rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
+    	fi; \
+    	\
+    # some of the steps above generate a lot of "*.pyc" files (and setting "PYTHONDONTWRITEBYTECODE" beforehand doesn't propagate properly for some reason), so we clean them up manually (as long as they aren't owned by a package)
+    	find /usr -name '*.pyc' -type f -exec bash -c 'for pyc; do dpkg -S "$pyc" &> /dev/null || rm -vf "$pyc"; done' -- '{}' +
+    
+    # make the sample config easier to munge (and "correct by default")
+    RUN set -eux; \
+    	dpkg-divert --add --rename --divert "/usr/share/postgresql/postgresql.conf.sample.dpkg" "/usr/share/postgresql/$PG_MAJOR/postgresql.conf.sample"; \
+    	cp -v /usr/share/postgresql/postgresql.conf.sample.dpkg /usr/share/postgresql/postgresql.conf.sample; \
+    	ln -sv ../postgresql.conf.sample "/usr/share/postgresql/$PG_MAJOR/"; \
+    	sed -ri "s!^#?(listen_addresses)\s*=\s*\S+.*!\1 = '*'!" /usr/share/postgresql/postgresql.conf.sample; \
+    	grep -F "listen_addresses = '*'" /usr/share/postgresql/postgresql.conf.sample
+    
+    RUN mkdir -p /var/run/postgresql && chown -R postgres:postgres /var/run/postgresql && chmod 2777 /var/run/postgresql
+    
+    ENV PATH $PATH:/usr/lib/postgresql/$PG_MAJOR/bin
+    ENV PGDATA /var/lib/postgresql/data
+    # this 777 will be replaced by 700 at runtime (allows semi-arbitrary "--user" values)
+    RUN mkdir -p "$PGDATA" && chown -R postgres:postgres "$PGDATA" && chmod 777 "$PGDATA"
+    VOLUME /var/lib/postgresql/data
+    
+    COPY docker-entrypoint.sh /usr/local/bin/
+    RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat
+    ENTRYPOINT ["docker-entrypoint.sh"]
+    
+    RUN apt-get update && apt-get install -y "postgresql-plpython3-12"
+    
+    # RUN echo 'CREATE EXTENSION plpythonu;' > /docker-entrypoint-initdb.d/plpythonu.sql
+    
+    EXPOSE 5432
+    CMD ["postgres"]
+</details>
+
+    3. 文件内容说明
+        1). docker-entrypoint.sh 是postgresql数据库的配置文件, 会拷贝到镜像内部使用
+        2). Dockerfile是docker用来构建镜像的文件, 分为以下几个部分 
+            1. 安装python3
+            2. 安装python模块 vg, matplotlib, shapely, jieba, short-text-sim, networkx, numpy, git.sagacloud.cn上的算法包 
+            3. 安装postgresql数据库
+            4. 安装plpython3插件
+    以上是镜像文件构建的准备工作
+
+## 构建镜像
+
+    1. 在dockerTemp临时工作目录内构建镜像
+        docker build -t mypg:1 .
+        其中-t是给镜像起个名字, mypg是名字, 2是版本号, 最后还有一个.表示基于当前目录进行构建镜像, 如果初次执行, 该命令耗时较长
+
+## 运行镜像, 开启服务
+
+    1. 查看镜像
+        docker images
+![pg002](./img/pgdocker_002.png)
+
+    2. 运行
+        mkdir -p /var/lib/postgresql/data
+        docker run -d -e POSTGRES_PASSWORD=123456 --restart=always --name pg12 -v /var/lib/postgresql/data:/var/lib/postgresql/data -p 5432:5432 mypg:1 postgres
+        
+        -d表示后台执行 (容器有个特性, 如果你run容器时执行的命令结束的话, 容器则自动结束) 
+        -e表示设置环境变量(此处为pg数据库密码)
+        --restart=always 表示自动重启策略 (策略类型有 no, on-failure, unless-stopped, always)
+        --name是给容器起一个名字pg12
+        -p 表示端口映射(本机端口:容器端口)
+        -v 表示文件夹映射(本机目录:容器目录)  此处表示postgresql的数据文件存放目录放在本机中, 即便换了容器, 数据也是不变
+        postgres 表示开启容器后执行的命令(postgres表示开启postgresql数据库)
+    3. 检测
+        docker ps         (查看当前运行中的容器 -a是查看所有)
+        如下图所示, 服务已经开启, 可以测试连接数据库
+![pg003](./img/pgdocker_003.jpg)
+
+## 查看pg数据库数据文件的映射(volumes 映射)
+    
+    volumes文件映射的功能是让容器和本机的文件夹数据共享的机制
+        docker inspect pg12 | grep Mounts -A 20
+    
+![pg004](./img/pgdocker_004.jpg)
+
+## 其他常用命令
+    
+    1. 查看所有容器
+        docker ps -a
+    2. 查看所有镜像
+        docker images
+    3. 关闭容器, 开启容器
+        docker start pg12
+        docker stop pg12
+    4. 删除容器
+        docker rm pg12  (需要先关闭容器)
+        docker rm -f pg12  (强制删除)
+    5. 删除镜像
+        docker rmi [镜像id]
+    6. 进入容器
+        docker exec -it pg12 /bin/bash
+        (帮助: docker exec --help)
+    7. 退出容器
+        exit/quit
+        ctrl + P + Q (不关闭当前进程)
+    8. 修改容器配置
+        docker update --restart=always pg12    (设置容器的重启规则)
+        
+## 导出和载入镜像
+    镜像的导入和导出是方便镜像的分享和迁移, 如果服务器网路不好或者pull不下来镜像,只能在其它网路比较好的机器上pull下来
+    镜像,导出成一个文件,再下载上传到网路不好的机器上,然后再从文件中导出来,这样在网络不好的机器上也能使用docker镜像
+    
+    1. 使用 docker images 找到需要导出的镜像
+    2. 导出镜像, 将镜像保存为文件 
+        docker save 镜像id > /root/myimage.tar
+    3. 将镜像保存到另一台不能联网的服务器(需要安装过docker)上, 加载镜像
+        docker load < /root/myimage.tar
+    4. (选作) 有可能在新服务器上不能识别镜像的tag(名称:版本), 需要执行
+        docker tag 镜像id 名称:版本       (例如: docker tag eb40dcf64078 nginx:latest)

+ 1 - 0
docs/setup/centos/server/python.md

@@ -24,6 +24,7 @@ CentOS7默认软件源中没有Python3,要安装Python3需首先安装epel软
 [root@localhost ~] pip3 install shapely
 [root@localhost ~] pip3 install networkx
 [root@localhost ~] pip3 install jieba
+[root@localhost ~] pip3 install short-text-sim -i http://dev.dp.sagacloud.cn:8082/repository/saga-pypi-group/simple --trusted-host dev.dp.sagacloud.cn
 ```
 ### 安装过程遇到的问题 
 

+ 9 - 1
docs/setup/index.js

@@ -21,6 +21,7 @@ const content = [
                     ["/setup/centos/server/nginx", "Nginx"],
                     ["/setup/centos/server/tomcat", "Tomcat"],
                     ["/setup/centos/server/postgresql", "PostgreSQL"],
+                    ["/setup/centos/server/postgresql-docker", "PostgreSQL(Docker版)"],
                     ["/setup/centos/server/kafka", "Kafka"]
                 ]
             },
@@ -31,8 +32,15 @@ const content = [
                     ["/setup/centos/app-server/datacenter", "数据中心datacenter"],
                     // ["/setup/centos/app-server/pointconfig", "点位配置"],
                     ["/setup/centos/app-server/ibms", "用户信息服务ibms"],
+                    ["/setup/centos/app-server/revit-algorithm", "模型web服务"],
+                    ["/setup/centos/app-server/scheduler", "模型调度器"],
                     ["/setup/centos/app-server/equip-component", "慧云盈定制接口服务equip-component"],
-                    ["/setup/centos/app-server/dataplatform-sync", "数据同步服务dataplatform-sync"]
+                    ["/setup/centos/app-server/dataplatform-sync", "数据同步服务dataplatform-sync"],
+                    ["/setup/centos/app-server/MBI", "MBI操作流程(孟向歌)"],
+                    ["/setup/centos/app-server/revit-format", "revit导出数据格式"],
+                    ["/setup/centos/app-server/revit-service", "revit服务"],
+                    ["/setup/centos/app-server/revit-check", "模型检查数据格式"],
+                    ["/setup/centos/app-server/centos-postgresql-plpython3", "centos7 编译安装postgresql"]
                 ]
             }
         ]

+ 2 - 0
docs/setup/windows/server/python.md

@@ -35,4 +35,6 @@ D:\SDK\Python37> pip3 install numpy
 D:\SDK\Python37> pip3 install matplotlib
 D:\SDK\Python37> pip3 install shapely
 D:\SDK\Python37> pip3 install networkx
+D:\SDK\Python37> pip3 install jieba
+D:\SDK\Python37> pip3 install short-text-sim -i http://dev.dp.sagacloud.cn:8082/repository/saga-pypi-group/simple --trusted-host dev.dp.sagacloud.cn
 ```