فهرست منبع

Merge remote-tracking branch 'origin/master'

张维新 4 سال پیش
والد
کامیت
0455c60d22
76فایلهای تغییر یافته به همراه110765 افزوده شده و 401 حذف شده
  1. 3 0
      docs/.vuepress/components/example/web/graph/.idea/.gitignore
  2. 6 0
      docs/.vuepress/components/example/web/graph/.idea/misc.xml
  3. 8 0
      docs/.vuepress/components/example/web/graph/.idea/modules.xml
  4. 6 0
      docs/.vuepress/components/example/web/graph/.idea/vcs.xml
  5. 0 1
      docs/.vuepress/components/example/web/graph/Circle1.vue
  6. 53 0
      docs/.vuepress/components/example/web/graph/MapDemo.vue
  7. 184 0
      docs/.vuepress/components/example/web/graph/scene/Align.vue
  8. 6 4
      docs/.vuepress/components/example/web/graph/scene/ImageItem.vue
  9. 55 0
      docs/.vuepress/components/example/web/graph/scene/PolylineItem.vue
  10. 22 228
      docs/.vuepress/components/example/web/graph/scene/SEditLine.vue
  11. 21 18
      docs/.vuepress/components/example/web/graph/scene/SImgTextItem.vue
  12. 25 27
      docs/.vuepress/components/example/web/graph/scene/TextItem.vue
  13. 83 0
      docs/.vuepress/components/example/web/graph/style/shadow.vue
  14. 4 4
      docs/.vuepress/config.js
  15. 77122 0
      docs/.vuepress/public/assets/map/1.json
  16. BIN
      docs/.vuepress/public/assets/map/1.jsonz
  17. 30621 0
      docs/.vuepress/public/assets/map/2.json
  18. BIN
      docs/.vuepress/public/assets/map/2.jsonz
  19. 3 0
      docs/.vuepress/styles/palette.styl
  20. 14 0
      docs/.vuepress/templates/dev.html
  21. 20 0
      docs/.vuepress/templates/ssr.html
  22. 1 1
      docs/dev/data-center-scheme/index.js
  23. 17 1
      docs/dev/data-center/index.js
  24. 6 0
      docs/dev/data-center/relations/belongs/Bd2Sp.md
  25. 5 0
      docs/dev/data-center/relations/belongs/Eq2Bd.md
  26. 6 0
      docs/dev/data-center/relations/belongs/Eq2Sh.md
  27. 1 0
      docs/dev/data-center/relations/belongs/Eq2Sp.md
  28. 5 0
      docs/dev/data-center/relations/belongs/Fl2Fl.md
  29. 6 0
      docs/dev/data-center/relations/belongs/Pe2Bd.md
  30. 6 0
      docs/dev/data-center/relations/belongs/Pe2Fl.md
  31. 6 0
      docs/dev/data-center/relations/belongs/Pe2Sh.md
  32. 6 0
      docs/dev/data-center/relations/belongs/Pe2Sp.md
  33. 5 0
      docs/dev/data-center/relations/belongs/Sh2Bd.md
  34. 6 0
      docs/dev/data-center/relations/belongs/Sh2Sh.md
  35. 9 3
      docs/dev/data-center/relations/belongs/Sp2Sp.md
  36. 31 11
      docs/dev/data-center/relations/belongs/Sp2Sp2.md
  37. 575 0
      docs/dev/data-center/relations/belongs/Sp2Sp_convection.md
  38. 533 0
      docs/dev/data-center/relations/belongs/Sp2Sp_light.md
  39. 0 34
      docs/dev/data-center/relations/belongs/Sp2Sp_new.md
  40. 493 0
      docs/dev/data-center/relations/belongs/Sp2Sp_transportation.md
  41. 8 2
      docs/dev/data-center/relations/belongs/Sy2Bd.md
  42. 8 2
      docs/dev/data-center/relations/belongs/Sy2Fl.md
  43. 8 2
      docs/dev/data-center/relations/belongs/Sy2Sh.md
  44. 7 1
      docs/dev/data-center/relations/belongs/Sy2Sp.md
  45. 11 2
      docs/dev/data-center/relations/belongs/f_rel_calc.md
  46. 0 0
      docs/dev/data-center/relations/belongs/img/sp2sp2-1.png
  47. BIN
      docs/dev/data-center/relations/belongs/img/sp2sp_light-1.png
  48. BIN
      docs/dev/data-center/relations/belongs/img/sp2sp_transportation-1.png
  49. 9 3
      docs/dev/data-center/relations/other/calc_area_v3.md
  50. BIN
      docs/dev/data-center/relations/other/img/rel_cilin-1.png
  51. 88 0
      docs/dev/data-center/relations/other/is_point_in_polygon_v1.md
  52. 126 0
      docs/dev/data-center/relations/other/is_point_in_polygon_v2.md
  53. 142 0
      docs/dev/data-center/relations/other/is_point_in_polygon_v3.md
  54. 34 13
      docs/dev/data-center/relations/other/calc_vertically_overlap.md
  55. 6 0
      docs/dev/data-center/relations/other/other_ispace_floor_overlap.md
  56. 5 5
      docs/dev/data-center/relations/other/rel_affected_space.md
  57. 43 0
      docs/dev/data-center/relations/other/rel_cilin_str_similar.md
  58. 5 5
      docs/dev/data-center/relations/other/rel_str_similar.md
  59. 110 0
      docs/dev/data-center/relations/other/sim.md
  60. BIN
      docs/dev/data-center/relations/problems/img/problem-1.png
  61. BIN
      docs/dev/data-center/relations/problems/img/problem-2.png
  62. BIN
      docs/dev/data-center/relations/problems/img/problem-3.png
  63. 60 0
      docs/dev/data-center/relations/problems/problems.md
  64. 19 0
      docs/dev/data-center/relations/problems/vwall_problem.md
  65. 20 13
      docs/dev/data-center/relations/topology/sys_block.md
  66. 13 7
      docs/dev/data-center/relations/topology/sys_direction.md
  67. 10 0
      docs/dev/saga-graphy/floor-map/mapDemo.md
  68. 5 0
      docs/dev/saga-graphy/graphy-engine/style.md
  69. 5 2
      docs/dev/saga-graphy/index.js
  70. 16 0
      docs/dev/saga-graphy/scene-manage/align.md
  71. 2 4
      docs/dev/saga-graphy/scene-manage/items/editLine.md
  72. 0 0
      docs/dev/saga-graphy/scene-manage/items/editPolygon.md
  73. 15 0
      docs/dev/saga-graphy/scene-manage/items/editPolyline.md
  74. 1 1
      docs/dev/saga-graphy/scene-manage/items/image.md
  75. 12 3
      docs/dev/saga-graphy/system-diagram/json-file.md
  76. 4 4
      package.json

+ 3 - 0
docs/.vuepress/components/example/web/graph/.idea/.gitignore

@@ -0,0 +1,3 @@
+
+# Default ignored files
+/workspace.xml

+ 6 - 0
docs/.vuepress/components/example/web/graph/.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+</project>

+ 8 - 0
docs/.vuepress/components/example/web/graph/.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/graph.iml" filepath="$PROJECT_DIR$/.idea/graph.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
docs/.vuepress/components/example/web/graph/.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$/../../../../../.." vcs="Git" />
+  </component>
+</project>

+ 0 - 1
docs/.vuepress/components/example/web/graph/Circle1.vue

@@ -1,4 +1,3 @@
-import {SLineCapStyle} from "@saga-web/draw/lib";
 <template>
     <canvas id="circle" width="740" height="250" />
 </template>

+ 53 - 0
docs/.vuepress/components/example/web/graph/MapDemo.vue

@@ -0,0 +1,53 @@
+<template>
+    <div>
+        <el-button @click="showMap(1)">查看地图1</el-button>
+        <el-button @click="showMap(2)">查看地图2</el-button>
+        <canvas id="mapDemo" width="740" height="400" tabindex="0"></canvas>
+    </div>
+</template>
+
+<script>
+    import { SGraphScene, SGraphView } from "@saga-web/graph/lib";
+    import { SFloorParser } from "@saga-web/big/lib";
+    export default {
+        name: "mapDemo",
+        data(){
+            return{
+                view:null,
+                scene:null,
+                map1: require('../../../../public/assets/map/1.json'),
+                map2: require('../../../../public/assets/map/2.json'),
+            }
+        },
+        mounted(){
+            this.view = new SGraphView("mapDemo");
+            this.init()
+        },
+        methods:{
+            init(){
+                this.showMap(1);
+            },
+            showMap(id){
+                this.scene = new SGraphScene();
+                this.view.scene = this.scene;
+                let parser = new SFloorParser();
+                parser.parseData(this[`map${id}`].EntityList[0].Elements);
+                parser.spaceList.forEach(t => this.scene.addItem(t));
+                parser.wallList.forEach(t => this.scene.addItem(t));
+                parser.virtualWallList.forEach(t => this.scene.addItem(t));
+                parser.doorList.forEach(t => this.scene.addItem(t));
+                parser.columnList.forEach(t => this.scene.addItem(t));
+                parser.casementList.forEach(t => this.scene.addItem(t));
+                this.view.fitSceneToView();
+            }
+        }
+    }
+</script>
+
+
+<style scoped>
+    canvas{
+        border: 1px solid #eeeeee;
+        outline: none;
+    }
+</style>

+ 184 - 0
docs/.vuepress/components/example/web/graph/scene/Align.vue

@@ -0,0 +1,184 @@
+<template>
+    <div>
+        <el-button @click="addCircle">Circle</el-button>
+        <el-button @click="addRect">Rect</el-button>
+        <el-select placeholder="请选择" @change="changeAlign" v-model="value">
+            <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
+        </el-select>
+        <canvas id="align" width="740" height="400" tabindex="0"></canvas>
+    </div>
+</template>
+
+<script>
+    import {SGraphItem, SGraphScene, SGraphView, SGraphLayoutType} from "@saga-web/graph/lib";
+    import { SColor, SPainter, SRect } from "@saga-web/draw/lib";
+
+    class RectItem extends SGraphItem {
+        width = 200;
+        height = 100;
+        text = '';
+        constructor(parent) {
+            super(parent);
+            this.moveable = true;
+            this.selectable = true;
+            this.text = new Date().getMilliseconds().toString()
+        }
+
+        boundingRect() {
+            return new SRect(0, 0, this.width, this.height);
+        }
+
+        onDraw(canvas) {
+            canvas.pen.color = SColor.Transparent;
+            canvas.pen.lineWidth = canvas.toPx(1);
+            canvas.brush.color = this.selected ? SColor.Red : new SColor("#00fcee");
+            canvas.drawRect(0, 0, this.width, this.height);
+
+            canvas.pen.lineDash = [10,10];
+            canvas.pen.color = SColor.Yellow;
+            canvas.brush.color = SColor.Transparent;
+            canvas.drawRect(this.boundingRect());
+
+            canvas.brush.color = SColor.Black;
+            canvas.drawText(`${this.x},${this.y}`,0,0);
+
+        }
+    }
+
+    class CircleItem extends SGraphItem {
+        r = 75;
+        text = '';
+
+        constructor(parent) {
+            super(parent);
+            this.moveable = true;
+            this.selectable = true;
+            this.text = new Date().getMilliseconds().toString()
+        }
+
+        boundingRect() {
+            return new SRect(0, 0, this.r * 2, this.r * 2);
+        }
+
+        onDraw(canvas) {
+            canvas.pen.color = SColor.Transparent;
+            canvas.pen.lineWidth = canvas.toPx(1);
+            canvas.brush.color = this.selected ? SColor.Red : new SColor("#00fcee");
+            canvas.drawCircle(this.r, this.r, this.r);
+
+            canvas.pen.color = SColor.Yellow;
+            canvas.brush.color = SColor.Transparent;
+            canvas.pen.lineDash = [10,10];
+            canvas.drawRect(this.boundingRect());
+
+            canvas.brush.color = SColor.Black;
+            canvas.drawText(`${this.x},${this.y}`,0,0);
+        }
+    }
+
+    class SScene extends SGraphScene {
+        /** 定义命令 */
+        cmd = 0;
+
+        constructor() {
+            super();
+        }
+
+
+        onMouseUp(event) {
+            switch(this.cmd) {
+                case 1:
+                    this.addCircle(event.x, event.y);
+                    break;
+                case 2:
+                    this.addRect(event.x, event.y);
+                    break;
+                default:
+                    super.onMouseUp(event);
+            }
+            this.cmd = 0;
+            return false
+        }
+
+        addCircle(x, y) {
+            let item = new CircleItem(null);
+            item.moveTo(x - 50, y - 50);
+            this.addItem(item);
+        }
+
+        addRect(x, y) {
+            let item = new RectItem(null);
+            item.moveTo(x - 50, y - 50);
+            this.addItem(item);
+        }
+    }
+
+    class TestView extends SGraphView {
+        constructor() {
+            super("align");
+        }
+    }
+
+    export default {
+        data() {
+            return {
+                scene: new SScene(),
+                value: -1,
+                options:[
+                    {
+                        value:SGraphLayoutType.Left,
+                        label:'左对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Right,
+                        label:'右对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Top,
+                        label:'顶对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Bottom,
+                        label:'底对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Center,
+                        label:'水平居中对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Middle,
+                        label:'垂直居中对齐'
+                    },
+                    {
+                        value:SGraphLayoutType.Vertical,
+                        label:'垂直分布'
+                    },
+                    {
+                        value:SGraphLayoutType.Horizontal,
+                        label:'水平分布'
+                    }
+                ]
+            }
+        },
+        mounted() {
+            let view = new TestView();
+            view.scene = this.scene;//new SScene(); //this.data.scene;
+        },
+        methods: {
+            addCircle() {
+                this.scene.cmd = 1;
+            },
+            addRect() {
+                this.scene.cmd = 2;
+            },
+            changeAlign(v){
+                this.scene.selectContainer.layout(v)
+                // console.log(this.scene.selectContainer.layout(SGraphLayoutType.Left))
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>

+ 6 - 4
docs/.vuepress/components/example/web/graph/scene/ImageItem.vue

@@ -10,9 +10,7 @@
 </template>
 
 <script lang="ts">
-    import { SGraphScene, SGraphView } from "@saga-web/graph";
-    import { SImageItem } from "@saga-web/big/lib";
-    import { SImageShowType } from "@saga-web/big/lib/enums/SImageShowType";
+    import { SGraphScene, SGraphView, SImageShowType, SImageItem } from "@saga-web/graph";
 
     class SScene extends SGraphScene {
         imageItem: SImageItem = new SImageItem(null);
@@ -35,6 +33,7 @@
     export default {
         mounted(): void {
             let view = new ImageView();
+            // @ts-ignore
             view.scene = this.scene;
             view.fitSceneToView();
         },
@@ -45,12 +44,15 @@
         },
         methods: {
             Full() {
+                // @ts-ignore
                 this.scene.imageItem.showType = SImageShowType.Full;
             },
             Equivalency() {
+                // @ts-ignore
                 this.scene.imageItem.showType = SImageShowType.Equivalency;
             },
             AutoFit() {
+                // @ts-ignore
                 this.scene.imageItem.showType = SImageShowType.AutoFit;
             },
         }
@@ -59,4 +61,4 @@
 
 <style scoped>
 
-</style>
+</style>

+ 55 - 0
docs/.vuepress/components/example/web/graph/scene/PolylineItem.vue

@@ -0,0 +1,55 @@
+<template>
+    <div>
+        <el-button size="mini" @click="init">清空画布,并初始化折线item</el-button>
+        <el-button size="mini" @click="undo">undo</el-button>
+        <el-button size="mini" @click="redo">redo</el-button>
+        <canvas id="editPolyline" width="740" height="400" tabindex="0"></canvas>
+    </div>
+</template>
+
+<script>
+    import {SGraphScene, SGraphView} from "@saga-web/graph/lib";
+    import {SPolylineItem} from "@saga-web/big/lib";
+    export default {
+        name: "editPolyline",
+        data() {
+            return {
+                scene: null,
+                view: null,
+                item: null,
+            };
+        },
+        mounted() {
+            this.view = new SGraphView("editPolyline");
+            this.scene = new SGraphScene();
+            this.view.scene = this.scene;
+            this.init()
+        },
+        methods:{
+            init(){
+                this.scene.root.children = [];
+                this.item = new SPolylineItem(null,[]);
+                this.scene.addItem(this.item);
+                this.scene.grabItem = this.item;
+                this.view.update();
+            },
+            undo(){
+                if(this.scene.grabItem) {
+                    this.scene.grabItem.undo()
+                }
+            },
+            redo(){
+                if(this.scene.grabItem) {
+                    this.scene.grabItem.redo()
+                }
+            }
+        }
+    }
+</script>
+
+<style scoped>
+canvas{
+    border: 1px solid #eeeeee;
+    outline: none;
+}
+</style>

+ 22 - 228
docs/.vuepress/components/example/web/graph/scene/SEditLine.vue

@@ -1,220 +1,15 @@
 <template>
     <div>
-        <el-button @click="show">展示</el-button>
         <el-button @click="create">创建</el-button>
-        <el-button @click="edit">编辑</el-button>
+        <el-button @click="undo">undo</el-button>
+        <el-button @click="redo">redo</el-button>
         <canvas id="editLine" width="740" height="400"  tabindex="0" />
     </div>
 </template>
-<script lang='ts'>
-    import {SGraphItem, SGraphScene, SGraphView} from "@saga-web/graph/";
-    import {SMouseEvent} from "@saga-web/base/";
-    import {SColor, SLine, SPainter, SPoint, SRect} from "@saga-web/draw";
-    import {SRelationState} from "@saga-web/big/lib/enums/SRelationState";
-    import {SMathUtil} from "@saga-web/big/lib/utils/SMathUtil";
-
-    /**
-     * 直线item
-     *
-     * */
-
-    class SLineItem extends SGraphItem {
-        /** 线段   */
-        line: SPoint[] = [];
-        /** 是否完成绘制  */
-        _status: SRelationState = SRelationState.Create;
-        get status(): SRelationState {
-            return this._status;
-        }
-        set status(v: SRelationState) {
-            this._status = v;
-            this.update();
-        }
-        /** 线条颜色    */
-        _strokeColor: string = "#000000";
-        get strokeColor(): string {
-            return this._strokeColor;
-        }
-        set strokeColor(v: string) {
-            this._strokeColor = v;
-            this.update();
-        }
-        /** 填充色 */
-        _fillColor: string = "#2196f3";
-        get fillColor(): string {
-            return this._fillColor;
-        }
-        set fillColor(v: string) {
-            this._fillColor = v;
-            this.update();
-        }
-        /** 线条宽度    */
-        _lineWidth: number = 1;
-        get lineWidth(): number {
-            return this._lineWidth;
-        }
-        set lineWidth(v: number) {
-            this._lineWidth = v;
-            this.update();
-        }
-        /** 拖动灵敏度   */
-        dis: number = 10;
-        /** 拖动灵敏度   */
-        sceneDis: number = 10;
-
-        /**
-         * 构造函数
-         *
-         * @param   parent  父级
-         * @param   line    坐标集合
-         * */
-        constructor(parent: SGraphItem | null, line: SPoint[]);
-
-        /**
-         * 构造函数
-         *
-         * @param   parent  父级
-         * @param   point   第一个点坐标
-         * */
-        constructor(parent: SGraphItem | null, point: SPoint);
-
-        /**
-         * 构造函数
-         *
-         * @param   parent  父级
-         * @param   l       坐标集合|第一个点坐标
-         * */
-        constructor(parent: SGraphItem | null, l: SPoint | SPoint[]) {
-            super(parent);
-            if (l instanceof SPoint) {
-                this.line.push(l);
-            } else {
-                this.line = l;
-            }
-        }
-
-        /**
-         * 鼠标按下事件
-         *
-         * @param   event   鼠标事件
-         * @return  是否处理事件
-         * */
-        onMouseDown(event: SMouseEvent): boolean {
-            console.log(this);
-            if (this.status == SRelationState.Normal) {
-                return super.onMouseDown(event);
-            } else if (this.status == SRelationState.Edit) {
-            } else if (this.status == SRelationState.Create) {
-
-            }
-            this.$emit("mousedown");
-            return true;
-        } // Function onMouseDown()
-
-        /**
-         * 鼠标移动事件
-         *
-         * @param   event   鼠标事件
-         * @return  是否处理事件
-         * */
-        onMouseMove(event: SMouseEvent): boolean {
-            if (this.status == SRelationState.Create) {
-
-                return true;
-            } else {
-                return super.onMouseMove(event);
-            }
-        } // Function onMouseMove()
-
-        /**
-         * 鼠标移动事件
-         *
-         * @param   event   鼠标事件
-         * @return  是否处理事件
-         * */
-        onMouseUp(event: SMouseEvent): boolean {
-            // if (this.status == SRelationState.Create) {
-            //     this.status = SRelationState.Edit;
-            //     return true;
-            // } else {
-                return super.onMouseUp(event);
-            // }
-        } // Function onMouseUp()
-
-        /**
-         * 判断点是否在区域内
-         *
-         * @param   x
-         * @param   y
-         * @return  true-是
-         */
-        contains(x: number, y: number): boolean {
-            if (this.line.length == 2) {
-                let p = new SPoint(x, y);
-                if (
-                    SMathUtil.pointToLine(p, new SLine(this.line[0], this.line[1])).MinDis <
-                    this.dis
-                ) {
-                    return true;
-                }
-            }
-            return false;
-        } // Function contains()
-
-        /**
-         * Item对象边界区域
-         *
-         * @return	SRect
-         */
-        boundingRect(): SRect {
-            if (this.line.length == 2) {
-                let x: number = this.line[0].x > this.line[1].x ? this.line[1].x : this.line[0].x;
-                let y: number = this.line[0].x > this.line[1].x ? this.line[1].y : this.line[0].y;
-                let width: number = Math.abs(this.line[0].x - this.line[1].x);
-                let height: number = Math.abs(this.line[0].y - this.line[1].y);
-                return new SRect(
-                    x,
-                    y,
-                    width,
-                    height
-                );
-            } else {
-                return new SRect();
-            }
-        } // Function boundingRect()
-
-        /**
-         * Item绘制操作
-         *
-         * @param   painter painter对象
-         */
-        onDraw(painter: SPainter): void {
-            this.sceneDis = painter.toPx(this.dis);
-            painter.pen.lineWidth = painter.toPx(this.lineWidth);
-            painter.pen.color = new SColor(this.strokeColor);
-            if (this.p1 instanceof SPoint && this.p2 instanceof SPoint) {
-                // 绘制外轮廓
-                painter.brush.color = SColor.Transparent;
-                painter.pen.color = new SColor("#128eee");
-                painter.drawRect(this.boundingRect());
-
-                // 绘制基本图形
-                painter.pen.color = new SColor(this.strokeColor);// 需注释
-                painter.drawLine(this.p1, this.p2);
-
-                // 编辑态
-                if (this.status == SRelationState.Edit) {
-                    painter.brush.color = SColor.White;
-
-                    // painter.brush.color = new SColor(this.fillColor);
-
-                    painter.drawCircle(this.p1.x, this.p1.y, painter.toPx(5));
-
-                    painter.drawCircle(this.p2.x, this.p2.y, painter.toPx(5));
-                }
-            }
-        } // Function onDraw()
-    } // Class SLineItem
+<script>
+    import { SGraphScene, SGraphView } from "@saga-web/graph/";
+    import { SItemStatus } from "@saga-web/big/lib/enums/SItemStatus";
+    import { SLineItem } from "@saga-web/big/lib";
 
     export default {
         data() {
@@ -223,30 +18,29 @@
                 view: null
             };
         },
-        mounted(): void {
+        mounted() {
             this.view = new SGraphView("editLine");
+            this.scene = new SGraphScene();
+            this.view.scene = this.scene;
         },
         methods: {
-            show() {
-                const scene = new SGraphScene();
-                this.view.scene = scene;
-                const line: SPoint[] = [new SPoint(100, 100), new SPoint(300, 300)];
-                const lineItem = new SLineItem(null, line);
-                lineItem.status = SRelationState.Normal;
-                scene.addItem(lineItem);
-                this.view.fitSceneToView();
-            },
             create() {
-                const scene = new SGraphScene();
-                this.view.scene = scene;
+                this.scene.root.children = [];
                 const lineItem = new SLineItem(null, [] );
-                lineItem.status = SRelationState.Create;
-                scene.addItem(lineItem);
-                scene.grabItem = lineItem;
+                lineItem.status = SItemStatus.Create;
+                this.scene.addItem(lineItem);
+                this.scene.grabItem = lineItem;
                 this.view.fitSceneToView();
             },
-            edit() {
-
+            undo(){
+                if(this.scene.grabItem) {
+                    this.scene.grabItem.undo();
+                }
+            },
+            redo(){
+                if(this.scene.grabItem) {
+                    this.scene.grabItem.redo();
+                }
             }
         }
     };

+ 21 - 18
docs/.vuepress/components/example/web/graph/scene/SImgTextItem.vue

@@ -7,9 +7,9 @@
 </template>
 
 <script lang="ts">
-    import {SGraphItem, SGraphScene, SGraphView} from "@saga-web/graph/lib";
-    import {SAnchorItem, SImageItem, SItemStatus, SObjectItem, STextItem} from "@saga-web/big/lib";
-    import {SColor, SPainter, SRect, SSize, STextBaseLine} from "@saga-web/draw/lib";
+    import {SGraphItem, SGraphScene, SGraphView, SObjectItem, STextItem, SImageItem, SAnchorItem} from "@saga-web/graph/lib";
+    import { SItemStatus }from "@saga-web/big/lib";
+    import {SColor, SPainter, SRect, SSize} from "@saga-web/draw/lib";
     import {SMouseEvent} from "@saga-web/base/lib";
 
     /**
@@ -94,10 +94,12 @@
                 return item;
             });
             this.update();
-            this.textItem.text = "请填写文本";
-            this.textItem.moveTo(16, -6);
+            this.textItem.text = "x2";
+            this.textItem.moveTo(18, -6);
             this.moveable = true;
             this.selectable = true;
+            this.textItem.enabled = false;
+            this.img.enabled = false;
         }
 
         onMouseEnter(event: SMouseEvent): boolean {
@@ -119,6 +121,7 @@
          *
          * */
         onMouseDown(event: SMouseEvent): boolean {
+            console.log(this.textItem)
             if (this.status == SItemStatus.Normal) {
                 return super.onMouseDown(event);
             } else if (this.status == SItemStatus.Edit) {
@@ -144,7 +147,6 @@
          * @return  是否处理事件
          * */
         onDoubleClick(event: SMouseEvent): boolean {
-            console.log('doubleclick');
             this.status = SItemStatus.Edit;
             return true;
         } // Function onDoubleClick()
@@ -155,14 +157,10 @@
          * @return  SRect   所有子对象的并集
          * */
         boundingRect(): SRect {
-            let rect :SRect = new SRect();
-            this.children.forEach(t => {
-                if (rect.isNull()) {
-                    rect = t.boundingRect().adjusted(t.pos.x,t.pos.y,0,0);
-                } else {
-                    rect = rect.unioned(t.boundingRect().adjusted(t.pos.x,t.pos.y,0,0));
-                }
-            });
+            let rect = this.img.boundingRect().adjusted(this.img.pos.x,this.img.pos.y,0,0);
+            if (this.showText) {
+                rect = rect.unioned(this.textItem.boundingRect().adjusted(this.textItem.pos.x,this.textItem.pos.y,0,0))
+            }
             return rect;
         } // Function boundingRect()
 
@@ -179,7 +177,6 @@
                 painter.brush.color = SColor.Gray
             }
             painter.drawRect(this.boundingRect());
-            // super.onDraw(painter);
         } // Function onDraw()
     }
 
@@ -195,26 +192,32 @@
         },
         mounted() {
             console.log(22222222222222222)
+            // @ts-ignore
             this.view = new SGraphView("editPolygon");
+            // @ts-ignore
             this.scene = new SGraphScene();
+            // @ts-ignore
             this.view.scene = this.scene;
+            // @ts-ignore
             this.init()
         },
         methods:{
             init(){
+                // @ts-ignore
                 this.item = new SImgTextItem(null);
+                // @ts-ignore
                 this.item.moveable = true;
 
+                // @ts-ignore
                 this.scene.addItem(this.item);
                 // this.view.fitSceneToView();
             },
-            change() {
-
-            },
             changemaodian(){
+                // @ts-ignore
                 this.item.showAnchor = !this.item.showAnchor;
             },
             changetext(){
+                // @ts-ignore
                 this.item.showText = !this.item.showText;
             }
         }

+ 25 - 27
docs/.vuepress/components/example/web/graph/scene/TextItem.vue

@@ -12,16 +12,14 @@
 	</div>
 </template>
 
-<script lang="ts">
-	import { SUndoStack } from "@saga-web/base";
-    import { SFont } from "@saga-web/draw";
-	import { SGraphItem, SGraphScene, SGraphView, SGraphPropertyCommand, SGraphMoveCommand } from "@saga-web/graph";
-	import { SPoint } from "@saga-web/draw/lib";
-	import {STextItem} from "@saga-web/big/lib";
+<script>
+	import { SUndoStack } from "@saga-web/base/lib";
+    import { SFont } from "@saga-web/draw/lib";
+	import { SGraphScene, SGraphView, SGraphPropertyCommand, SGraphMoveCommand, STextItem } from "@saga-web/graph/lib";
 
 	class SScene extends SGraphScene {
 		undoStack = new SUndoStack();
-		textItem: STextItem = new STextItem(null);
+		textItem = new STextItem(null);
 
 		constructor() {
 			super();
@@ -32,7 +30,7 @@
 			this.textItem.connect("onMove", this, this.onItemMove.bind(this));
 		}
 
-		updateText(str: string): void {
+		updateText(str) {
 			if (this.textItem.text !== str) {
 				let old = this.textItem.text;
 				this.textItem.text = str;
@@ -40,7 +38,7 @@
 			}
 		}
 
-		updateColor(color: string): void {
+		updateColor(color) {
 			if (this.textItem.color !== color) {
 				let old = this.textItem.color;
 				this.textItem.color = color;
@@ -48,7 +46,7 @@
 			}
 		}
 
-		updateSize(size: number): void {
+		updateSize(size) {
 			if (this.textItem.font.size !== size) {
 				let old = new SFont(this.textItem.font);
 				let font = new SFont(this.textItem.font);
@@ -58,8 +56,8 @@
 			}
 		}
 
-		onItemMove(item: SGraphItem, ...arg: any): void {
-            this.undoStack.push(new SGraphMoveCommand(this, item, arg[0][0] as SPoint, arg[0][1] as SPoint));
+		onItemMove(item, ...arg) {
+            this.undoStack.push(new SGraphMoveCommand(this, item, arg[0][0], arg[0][1]));
         }
 
 	}
@@ -71,13 +69,6 @@
 	}
 
     export default {
-        mounted(): void {
-			let view = new TestView();
-			this.scene.updateText(this.text);
-			this.scene.updateColor(this.color);
-			this.scene.updateSize(this.size);
-			view.scene = this.scene;
-		},
 		data() {
 			return {
 				scene: new SScene(),
@@ -86,23 +77,30 @@
 				color: "#333333",
 			}
 		},
+        mounted() {
+			let view = new TestView();
+			this.scene.updateText(this.text);
+			this.scene.updateColor(this.color);
+			this.scene.updateSize(this.size);
+			view.scene = this.scene;
+		},
 		methods: {
-			handleChangeText(text): void {
-				this.scene.updateText(this.text);
+			handleChangeText(text) {
+				this.scene.updateText(text);
 			},
-			handleChangeColor(color): void {
+			handleChangeColor(color) {
 				this.scene.updateColor(color);
 			},
-			handleChangeSize(size): void {
+			handleChangeSize(size) {
 				this.scene.updateSize(size);
 			},
-			undo(): void {
+			undo() {
 				this.scene.undoStack.undo();
 			},
-			redo(): void {
+			redo() {
 				this.scene.undoStack.redo();
 			},
-			reset(): void {
+			reset() {
 				this.text = "测试文本";
 				this.size = 20;
 				this.color = "#333333";
@@ -116,4 +114,4 @@
 
 <style scoped>
 
-</style>
+</style>

+ 83 - 0
docs/.vuepress/components/example/web/graph/style/shadow.vue

@@ -0,0 +1,83 @@
+<template>
+    <div>
+        阴影扩散范围:<el-input-number @change="changeblur" v-model="blurl" size="mini" style="width: 100px"></el-input-number>
+        x轴偏移量:<el-input-number @change="changeX" v-model="X" size="mini" style="width: 100px"></el-input-number>
+        y轴偏移量:<el-input-number @change="changeY" v-model="Y" size="mini" style="width: 100px"></el-input-number>
+        <el-color-picker @change="changeColor" v-model="color" size="mini" style="vertical-align: middle"></el-color-picker>
+        <canvas id="shadow" width="740" height="400" tabindex="0"></canvas>
+    </div>
+</template>
+
+<script>
+    import {SGraphScene, SGraphView} from "@saga-web/graph/lib";
+    import { SCanvasView, SColor, SPainter } from "@saga-web/draw/lib";
+
+    class shadowView extends SCanvasView {
+
+        constructor(id) {
+            super(id);
+            this.blurArr = [1,2,3,4,5,6,7,8,9,10];
+            this.shadowBlur = 10;
+            this.shadowColor = new SColor('#CCCCCC');
+            this.shadowOffsetX = 10;
+            this.shadowOffsetY = 10;
+        }
+
+        onDraw(canvas) {
+            canvas.clearRect(0,0,740,400);
+
+            canvas.pen.lineWidth = 1;
+            canvas.pen.color = SColor.Black;
+            canvas.brush.color = SColor.White;
+            canvas.shadow.shadowBlur = this.shadowBlur;
+            canvas.shadow.shadowColor = this.shadowColor;
+            canvas.shadow.shadowOffsetX = this.shadowOffsetX;
+            canvas.shadow.shadowOffsetY = this.shadowOffsetY;
+            canvas.drawRect(270,100,100,100);
+        }
+    }
+    export default {
+        name: "shadow",
+        data(){
+            return {
+                view: null,
+                blurl: 10,
+                X:10,
+                Y:10,
+                color:"#CCCCCC"
+            }
+        },
+        mounted() {
+            this.view = new shadowView('shadow');
+        },
+        methods:{
+            // 修改扩散距离
+            changeblur(v) {
+                this.view.shadowBlur = v;
+                this.view.update()
+            },
+            // x轴偏移量
+            changeX(v){
+                this.view.shadowOffsetX = v;
+                this.view.update()
+            },
+            // y轴偏移量
+            changeY(v){
+                this.view.shadowOffsetY = v;
+                this.view.update()
+            },
+            // 修改颜色
+            changeColor(v){
+                this.view.shadowColor = new SColor(v);
+                this.view.update()
+            }
+        }
+    }
+</script>
+
+<style scoped>
+    canvas{
+        border: 1px solid #eeeeee;
+        outline: none;
+    }
+</style>

+ 4 - 4
docs/.vuepress/config.js

@@ -72,9 +72,9 @@ module.exports = {
                         text: "新版数据字典物理世界对接",
                         items: [
                             { text: "新版字典中台工具接口对接", link: "/dev/data-center-scheme/" },
-                            { text: "新版字典ADM对接", link: "/dev/data-center-scheme/" },
-                            { text: "新版字典对接数据中心", link: "/dev/data-center-scheme/" },
-                            { text: "新版物理世界对接数据中心", link: "/dev/data-center-scheme/" }
+                            // { text: "新版字典ADM对接", link: "/dev/data-center-scheme/" },
+                            // { text: "新版字典对接数据中心", link: "/dev/data-center-scheme/" },
+                            // { text: "新版物理世界对接数据中心", link: "/dev/data-center-scheme/" }
                         ]
                     },
                 ]
@@ -94,7 +94,7 @@ module.exports = {
                     {
                         text: "Web开发",
                         items: [
-                            { text: "图形引擎", link: "http://adm.sagacloud.cn:8080/api/web/graphy/" },
+                            { text: "图形引擎", link: "http://adm.sagacloud.cn:8080/api/web/graph/" },
                             { text: "建筑信息图", link: "http://adm.sagacloud.cn:8080/api/web/big/" }
                         ]
                     },

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 77122 - 0
docs/.vuepress/public/assets/map/1.json


BIN
docs/.vuepress/public/assets/map/1.jsonz


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 30621 - 0
docs/.vuepress/public/assets/map/2.json


BIN
docs/.vuepress/public/assets/map/2.jsonz


+ 3 - 0
docs/.vuepress/styles/palette.styl

@@ -0,0 +1,3 @@
+body{
+  user-select :none;
+}

+ 14 - 0
docs/.vuepress/templates/dev.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width,initial-scale=1">
+    <title></title>
+</head>
+<body>
+<script>
+    const Image = window.Image;//定义global
+</script>
+<div id="app"></div>
+</body>
+</html>

+ 20 - 0
docs/.vuepress/templates/ssr.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="{{ lang }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width,initial-scale=1">
+    <title>{{ title }}</title>
+    <meta name="generator" content="VuePress {{ version }}">
+    {{{ userHeadTags }}}
+    {{{ pageMeta }}}
+    {{{ renderResourceHints() }}}
+    {{{ renderStyles() }}}
+    <script>
+        const Image = window.Image;//定义global变量
+    </script>
+</head>
+<body>
+<!--vue-ssr-outlet-->
+{{{ renderScripts() }}}
+</body>
+</html>

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

@@ -1,6 +1,6 @@
 const content = [
 
-            ["/dev/data-center-scheme/dict-dataplatform", "中台工具字典接口使用对接方案"],
+            // ["/dev/data-center-scheme/dict-dataplatform", "中台工具字典接口使用对接方案"],
             ["/dev/data-center-scheme/dict-ADM", "新版字典ADM对接方案"],
             ["/dev/data-center-scheme/data-center-platform", "数据中心对接物理世界"]
 

+ 17 - 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", "系统所在竖井"],
@@ -44,10 +47,15 @@ const content = [
         path: "/dev/data-center/relations/",
         children: [
             ["/dev/data-center/relations/other/calc_area_v3", "计算业务空间面积"],
-            ["/dev/data-center/relations/other/calc_vertically_overlap", "计算业务空间是否在垂直方向重叠"],
             ["/dev/data-center/relations/other/other_ispace_floor_overlap", "计算元空间轮廓是否与楼层轮廓重叠"],
             ["/dev/data-center/relations/other/rel_affected_space", "计算受影响的业务空间"],
             ["/dev/data-center/relations/other/rel_str_similar", "计算字符串相似度"],
+            ["/dev/data-center/relations/other/is_vertically_overlap", "业务空间垂直方向的交通关系"],
+            ["/dev/data-center/relations/other/is_point_in_polygon_v1", "判断点是否在楼层轮廓内"],
+            ["/dev/data-center/relations/other/is_point_in_polygon_v2", "判断点是否在元空间轮廓内"],
+            ["/dev/data-center/relations/other/is_point_in_polygon_v3", "判断点是否在业务空间轮廓内"],
+            ["/dev/data-center/relations/other/rel_cilin_str_similar", "词林相似度算法"],
+            ["/dev/data-center/relations/other/sim", "词林相似度算法详细文档"],
         ]
     },
     {
@@ -59,6 +67,14 @@ const content = [
         ]
     },
     {
+        title: "现存问题",
+        path: "/dev/data-center/relations/",
+        children: [
+            ["/dev/data-center/relations/problems/problems", "现存问题"]
+            ["/dev/data-center/relations/problems/vwall_problem", "虚拟墙问题"]
+        ]
+    },
+    {
         title: "代码说明",
         path: "/dev/data-center/code/",
         children: []

+ 6 - 0
docs/dev/data-center/relations/belongs/Bd2Sp.md

@@ -16,6 +16,10 @@ update zone_space_base zone set zone.building_id = floor.building_id from floor
 ```
 
 # 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 CREATE OR REPLACE FUNCTION "public"."rel_bd2sp"("tables" text, "project_id" varchar)
   RETURNS "pg_catalog"."bool" AS $BODY$
@@ -37,6 +41,8 @@ $BODY$
   COST 100
 ```
 
+</details>
+
 ## 输入
     1. 参与计算的表的全名称, 带schema名, 以英文逗号隔开
     2. 项目id

+ 5 - 0
docs/dev/data-center/relations/belongs/Eq2Bd.md

@@ -20,6 +20,9 @@ UPDATE equipment SET building_id = (SELECT building_id FROM "floor" WHERE id = e
 
 # 函数
 
+<details>
+<summary>源码</summary>
+
 ```plpython
 create or replace function public.rel_eq2bd(project_id character varying) returns boolean
 as
@@ -38,6 +41,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ```
 
+</details>
+
 
 ## 入参
     1. 项目id

+ 6 - 0
docs/dev/data-center/relations/belongs/Eq2Sh.md

@@ -10,6 +10,10 @@
     1. 通过 竖井 --> 业务空间 --> 设备 的间接所属关系, 推出竖井和设备的关系
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 设备所在竖井
 create or replace function public.rel_eq2sh(project_id character varying) returns boolean
@@ -44,6 +48,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ```
 
+</details>
+
 ## 入参
     1. 项目id
     

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

@@ -12,6 +12,7 @@
 
 # 函数
 ## 代码
+
 <details>
 <summary>源码</summary>
 

+ 5 - 0
docs/dev/data-center/relations/belongs/Fl2Fl.md

@@ -11,6 +11,9 @@ FROM floor WHERE project_id='Pj4201050001' AND model_id is not null
 
 # 函数
 
+<details>
+<summary>源码</summary>
+
 ```
 create or replace function public.rel_fl2fl(project_id character varying, sign1 integer, sign2 integer) returns boolean
 as
@@ -39,6 +42,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ```
 
+</details>
+
 ## 输入
     1. 项目id
     2. 表示r_fl_through_fl表中要被删除的sign值

+ 6 - 0
docs/dev/data-center/relations/belongs/Pe2Bd.md

@@ -8,6 +8,10 @@
     根据 资产 --> 设备/部件 --> 建筑 的间接从属关系, 来获取资产和建筑的所在关系
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 资产属于建筑体
 create or replace function public.rel_pe2bd(project_id character varying) returns boolean
@@ -30,6 +34,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_pe2bd('Pj1101010015');
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 6 - 0
docs/dev/data-center/relations/belongs/Pe2Fl.md

@@ -8,6 +8,10 @@
     根据 资产 --> 设备/部件 --> 楼层 的间接关系, 来获取资产所在建筑的关系
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 资产属于楼层
 create or replace function public.rel_pe2fl(project_id character varying) returns boolean
@@ -29,6 +33,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_pe2fl('Pj1101010015');
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 6 - 0
docs/dev/data-center/relations/belongs/Pe2Sh.md

@@ -8,6 +8,10 @@
     根据 资产 --> 设备/部件 --> 竖井 的间接关系, 来获取资产所在竖井的关系
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 资产所在竖井
 create or replace function public.rel_pe2sh(project_id character varying) returns boolean
@@ -32,6 +36,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_pe2sh('Pj1101010015');
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 6 - 0
docs/dev/data-center/relations/belongs/Pe2Sp.md

@@ -8,6 +8,10 @@
     根据 资产 --> 设备/部件 --> 业务空间 的间接关系, 来获取资产所在业务空间的关系
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 资产所在业务空间
 create or replace function public.rel_pe2sp(project_id character varying) returns boolean
@@ -37,6 +41,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_pe2sp('Pj1101010015');
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 5 - 0
docs/dev/data-center/relations/belongs/Sh2Bd.md

@@ -9,6 +9,9 @@
     
 # 函数
 
+<details>
+<summary>源码</summary>
+
 ```
 
 CREATE OR REPLACE FUNCTION "public"."rel_sh2bd"("project_id" varchar)
@@ -41,6 +44,8 @@ $BODY$
 select public.rel_sh2bd('Pj1102290002')
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 6 - 0
docs/dev/data-center/relations/belongs/Sh2Sh.md

@@ -5,6 +5,10 @@
     1. 竖井包含有相同的元空间, 则认为两竖井贯通
     (竖井包含元空间的数据是前端做划分)
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 竖井贯通关系
 create or replace function public.rel_sh2sh(project_id character varying) returns boolean
@@ -52,6 +56,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_sh2sh('Pj1102290002')
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

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

@@ -1,4 +1,4 @@
-业务空间邻接关系
+# 业务空间邻接关系
 ## 前置条件
     1. 业务空间有所在楼层
     2. 业务空间有外轮廓
@@ -12,6 +12,10 @@
     2. 如果有近似平行的线段, 判断是否相互有投影在线段上, 有投影在线段上, 则认为是两平行线段有重合部分, 业务空间有相邻的可能性
     3. 再判断互相有投影点在对方线段上之后, 判断投影线的长度, 是否小于250mm(墙的最大厚度), 如果小于250mm则认为两空间相邻
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 create or replace function public.rel_sp2sp1(project_id character varying) returns boolean
 as
@@ -20,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']])
@@ -42,6 +46,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.rel_sp2sp1('Pj1101050001')
 ```
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

+ 31 - 11
docs/dev/data-center/relations/belongs/Sp2Sp2.md

@@ -1,4 +1,4 @@
-业务空间邻接关系
+# 业务空间邻接关系
 ## 前置条件
     1. 业务空间有所在楼层
     2. 业务空间有外轮廓
@@ -15,19 +15,39 @@
     2. 将上面找到的点, 依次与组内其他业务空间外轮廓做一次点是否在多边形内的判断, 如果在, 则认为该多边形与图中多边形相邻
     
    
-![Image text](./img/sp2sp2-1.PNG)
+![image](./img/sp2sp2-1.png)
+
 
 ## 函数
 
+###  业务空间轮廓结构
+```
+[
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的外轮廓
+        [
+            {点坐标},
+            {点坐标}...
+        ], // 子轮廓的第n个需要被排除的轮廓
+    ], // 子轮廓
+    [
+        [
+            {点坐标},
+            {点坐标}...
+        ]
+    ],
+]
+```
 
 <details>
 <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
@@ -278,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)))
@@ -296,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');

+ 0 - 34
docs/dev/data-center/relations/belongs/Sp2Sp_new.md

@@ -1,34 +0,0 @@
-计算业务空间邻接关系
-# 函数依赖于
-    sp2fl
-# 业务空间轮廓结构
-```
-[
-    [
-        [
-            {点坐标},
-            {点坐标}...
-        ], // 子轮廓的外轮廓
-        [
-            {点坐标},
-            {点坐标}...
-        ], // 子轮廓的排除轮廓n
-    ], // 子轮廓
-    [
-        [
-            {点坐标},
-            {点坐标}...
-        ]
-    ],
-]
-```
-# 计算细节
-## 参与计算的业务空间
-    1. 找到项目下所有的楼层
-    2. 按照楼层, 分别找出每个楼层下的业务空间, 将一个楼层下的业务空间分组, 在一个组内计算业务空间的相邻关系
-## 在每个子轮廓的外轮廓的相邻两个顶点的线段上做"开门"操作
-    
-    1. 在线段长度大于等于一个门宽度的时候, 虚拟的在这个线段的两端分别各安装一扇门, 在门打开的过程中(两边都能打开), 扫过的面积如果跟其他空间(比如空间A)的面积有重叠时, 
-    则认为这个空间与空间A相邻, 依次找出所有与该空间相邻的空间.
-    2. 在线段长度不超过一个门的宽度时, 将继续寻找下一个点, 直到长度大于等于一个门的宽度时, 执行1
-    将此过程应用到所有空间上, 找出所有业务空间的相邻关系

+ 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');

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

@@ -1,4 +1,4 @@
-系统所在建筑
+# 系统所在建筑
 ## 前置条件
     1. 系统下有设备
     2. 系统下的设备有所属建筑的关系
@@ -6,9 +6,13 @@
     Eq2Bd
 ## 处理逻辑
     1. 通过 系统 --> 设备 --> 建筑 的间接关系, 获取系统所在建筑的关系
-    2. 结果存储在 r_sy_in_bd 表中
+    2. 结果存储在 r_sy_in_bd 表中 (先删除以前的自动计算结果)
     
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ~~~
 -- 系统所在建筑
 CREATE OR REPLACE FUNCTION "public"."rel_sy2bd"("project_id" varchar)
@@ -45,6 +49,8 @@ $BODY$
   COST 100
 ~~~
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

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

@@ -1,4 +1,4 @@
-系统所在楼层
+# 系统所在楼层
 ## 前置条件
     1. 系统下有设备
     2. 系统下的设备有所属楼层关系
@@ -6,8 +6,12 @@
     Eq2Fl
 ## 处理逻辑
     1. 通过 系统 --> 设备 --> 楼层 的间接关系, 获取系统所在楼层的关系
-    2. 计算关系存储在表 r_sy_in_fl 中
+    2. 计算关系存储在表 r_sy_in_fl 中 (先删除以前的自动计算结果)
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ~~~
 -- 系统所在楼层
 create or replace function public.rel_sy2fl(project_id character varying) returns boolean
@@ -42,6 +46,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ~~~
 
+</details>
+
 ## 入参
     1. 项目id
 ## 例子

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

@@ -1,4 +1,4 @@
-系统所在竖井
+# 系统所在竖井
 ## 前置条件
     1. 系统下有设备
     2. 系统下的设备有所在竖井关系
@@ -6,8 +6,12 @@
     Eq2Sh
 ## 处理逻辑
     1. 根据 系统 --> 设备 --> 竖井 来间接获取系统所属竖井关系
-    2. 计算后的关系存储在表 r_sy_in_sh
+    2. 计算后的关系存储在表 r_sy_in_sh (先删除以前的自动计算结果)
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 系统所在竖井
 create or replace function public.rel_sy2sh(project_id character varying) returns boolean
@@ -41,6 +45,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ```
 
+</details>
+
 ## 输入
     1. 项目id
 

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

@@ -6,8 +6,12 @@
     Eq2Sp
 ## 处理逻辑
     1. 根据 系统 --> 设备 --> 业务空间 的间接关系, 获取系统和业务空间的关系
-    2. 计算结果存储在r_sy_in_sp_*
+    2. 计算结果存储在r_sy_in_sp中 (先删除以前的自动计算结果)
 ## 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 -- 系统所在楼层
 create or replace function public.rel_sy2sp(project_id character varying) returns boolean
@@ -55,6 +59,8 @@ $BODY$
   COST 100
 ```
 
+</details>
+
 ## 输入
     1. 项目id
 

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

@@ -7,16 +7,22 @@
 # 函数
 ## 代码
 
+<details>
+<summary>源码</summary>
+
 ```
 CREATE OR REPLACE FUNCTION "public"."f_rel_calc"("func" text, "project_id" varchar, "tables" text, "out_tables" text, "sign1" int4, "sign2" int4)
   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)
@@ -80,6 +86,9 @@ $BODY$
 例子:
 select public.f_rel_calc('rel_eq2sh', 'Pj*******', 'zone_space_base','relationship.r_eq2sp', 2, 2)
 ```
+
+</details>
+
 ## 输入
     1. 想要计算的函数名
     2. 项目id

docs/dev/data-center/relations/belongs/img/sp2sp2-1.PNG → docs/dev/data-center/relations/belongs/img/sp2sp2-1.png


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


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


+ 9 - 3
docs/dev/data-center/relations/other/calc_area_v3.md

@@ -1,17 +1,21 @@
 计算业务空间的面积
 ## 前置条件
 
-
+    业务空间有轮廓数据
 
 ## 处理逻辑
 
-根据业务空间轮廓的格式, 以此计算内部每个小轮廓的面积, 加和在一起为最终结果
-小轮廓面积计算: 计算首个子轮廓面积, 并减去随后所有子轮廓的面积. 结果为小轮廓面积
+    根据业务空间轮廓的格式, 以此计算内部每个小轮廓的面积, 加和在一起为最终结果
+    小轮廓面积计算: 计算首个子轮廓面积, 并减去随后所有子轮廓的面积. 结果为小轮廓面积
 
 ## 实现方式
 
 
 # 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 CREATE OR REPLACE FUNCTION "public"."calc_area_v3"("json_poly" jsonb)
   RETURNS "pg_catalog"."float8" AS $BODY$
@@ -66,6 +70,8 @@ $BODY$
   COST 100
 ```
 
+</details>
+
 ## 输入
     1. 业务空间的轮廓
 

BIN
docs/dev/data-center/relations/other/img/rel_cilin-1.png


+ 88 - 0
docs/dev/data-center/relations/other/is_point_in_polygon_v1.md

@@ -0,0 +1,88 @@
+# 判断一个点是否在楼层的轮廓里
+## 前置条件
+    
+    楼层必须有轮廓, 且合法
+    
+## 处理逻辑
+
+    1. 楼层外轮廓数据只有一个由点组成的数组围成, 因此直接判断指定的点是否在点数组围成的多边形内即可
+
+## 楼层轮廓格式示例
+```
+
+[
+    {点坐标的XYZ信息},
+    {点坐标的XYZ信息}...
+]
+```
+
+
+# 函数
+## 版本1
+
+<details>
+<summary>输入为浮点型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v1"("x" float8, "y" float8, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+try:
+    poly = []
+    polygon = json.loads(json_poly)
+    l = len(polygon)
+    points = (x, y)
+    for i in range(l):
+        pair = polygon[i]
+        poly.append((pair["X"], pair["Y"]))
+
+    p = Path(poly)
+except Exception as e:
+    return False
+else:
+    return p.contains_points([points], None, -0.0001)
+
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 版本2
+<details>
+<summary>输入为字符串类型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v1_str"("x" varchar, "y" varchar, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+try:
+    poly = []
+    polygon = json.loads(json_poly)
+    l = len(polygon)
+    points = (float(x), float(y))
+    for i in range(l):
+        pair = polygon[i]
+        poly.append((pair["X"], pair["Y"]))
+
+    p = Path(poly)
+except Exception as e:
+    return False
+else:
+    return p.contains_points([points], None, -0.0001)
+
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 输入
+    1. 点的x坐标
+    2. 点的y坐标
+    3. 楼层的轮廓
+## 返回结果
+    true    成功
+    false   失败

+ 126 - 0
docs/dev/data-center/relations/other/is_point_in_polygon_v2.md

@@ -0,0 +1,126 @@
+# 判断一个点是否在元空间的轮廓里
+## 前置条件
+    
+    元空间必须有轮廓, 且合法
+    
+## 处理逻辑
+
+    1. 元空间外轮廓数据格式由如下示例所示
+    2. 需要判断点在外轮廓内, 且不在任意1个被排除的轮廓内
+
+## 元空间轮廓示例
+```
+
+[
+    [
+        {点坐标的XYZ信息},
+        {点坐标的XYZ信息}...
+    ], // 轮廓的外层轮廓
+    [
+        {点坐标的XYZ信息},
+        {点坐标的XYZ信息}...
+    ], // 轮廓的第n个需要被排除的轮廓, 可能有若干个
+]
+
+```
+
+
+# 函数
+## 版本1
+
+<details>
+<summary>输入为浮点型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v2"("x" float8, "y" float8, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+
+def is_in_meta_polygon(point, single_poly, radius):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair["X"], pair["Y"]))
+    p = Path(poly)
+    return p.contains_points([point], None, radius)
+
+try:
+    polygons = json.loads(json_poly)
+    total_len = len(polygons)
+    point_pair = (x, y)
+    if total_len == 0:
+        return False
+    for i in range(total_len):
+        polygon = polygons[i]
+        if(i == 0):
+            if not is_in_meta_polygon(point_pair, polygon, -0.001):
+                return False
+        else:
+            if is_in_meta_polygon(point_pair, polygon, 0.001):
+                return False
+    return True
+except Exception as e:
+    return False
+else:
+    return True
+
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 版本2
+<details>
+<summary>输入为字符串类型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v2_str"("x" varchar, "y" varchar, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+
+def is_in_meta_polygon(point, single_poly, radius):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair["X"], pair["Y"]))
+    p = Path(poly)
+    return p.contains_points([point], None, radius)
+
+try:
+    polygons = json.loads(json_poly)
+    total_len = len(polygons)
+    point_pair = (float(x), float(y))
+    if total_len == 0:
+        return False
+    for i in range(total_len):
+        polygon = polygons[i]
+        if(i == 0):
+            if not is_in_meta_polygon(point_pair, polygon, -0.001):
+                return False
+        else:
+            if is_in_meta_polygon(point_pair, polygon, 0.001):
+                return False
+    return True
+except Exception as e:
+    return False
+else:
+    return True
+
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 输入
+    1. 点的x坐标
+    2. 点的y坐标
+    3. 元空间的轮廓
+## 返回结果
+    true    成功
+    false   失败

+ 142 - 0
docs/dev/data-center/relations/other/is_point_in_polygon_v3.md

@@ -0,0 +1,142 @@
+# 判断一个点是否在楼层的轮廓里
+## 前置条件
+    
+    业务空间必须有轮廓, 且合法
+    
+## 处理逻辑
+
+    1. 业务空间外轮廓数据格式由如下示例所示, 由若干个元空间(子轮廓)的轮廓组成
+    2. 判断点在是否在某一个子轮廓内, 如果在, 则点在业务空间内(返回true)
+        1). 判断点在子轮廓内, 需要判断点在子轮廓的外轮廓内, 且不在任意1个子轮廓被排除的轮廓内
+
+##  业务空间轮廓结构示例
+```
+[
+    [
+        [
+            {点坐标的XYZ信息},
+            {点坐标的XYZ信息}...
+        ], // 子轮廓的外轮廓
+        [
+            {点坐标的XYZ信息},
+            {点坐标的XYZ信息}...
+        ], // 子轮廓的第n个需要被排除的轮廓, 可能有若干个
+    ], // 子轮廓
+    [
+        [
+            {点坐标的XYZ信息},
+            {点坐标的XYZ信息}...
+        ]
+    ],
+]
+```
+
+# 函数
+## 版本1
+
+<details>
+<summary>输入为浮点型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v3"("x" float8, "y" float8, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+
+def is_in_meta_polygon(point, single_poly, radius):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair["X"], pair["Y"]))
+    p = Path(poly)
+    return p.contains_points([point], None, radius)
+
+
+def is_in_polygon(point, polygons):
+    length = len(polygons)
+    if length == 0:
+        return False
+    for j in range(length):
+        polygon = polygons[j]
+        if j == 0:
+            if not is_in_meta_polygon(point, polygon, -0.001):
+                return False
+        else:
+            if is_in_meta_polygon(point, polygon, 0.001):
+                return False
+    return True
+
+try:
+    polygon_list = json.loads(json_poly)
+    total_len = len(polygon_list)
+    point_pair = (x, y)
+    for index in range(total_len):
+        if is_in_polygon(point_pair, polygon_list[index]):
+            return True
+    return False
+except Exception as e:
+    return False
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 版本2
+<details>
+<summary>输入为字符串类型</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."is_point_in_polygon_v3_str"("x" varchar, "y" varchar, "json_poly" jsonb)
+  RETURNS "pg_catalog"."bool" AS $BODY$
+from matplotlib.path import Path
+import json
+
+def is_in_meta_polygon(point, single_poly, radius):
+    poly_len = len(single_poly)
+    poly = []
+    for i in range(poly_len):
+        pair = single_poly[i]
+        poly.append((pair["X"], pair["Y"]))
+    p = Path(poly)
+    return p.contains_points([point], None, radius)
+
+
+def is_in_polygon(point, polygons):
+    length = len(polygons)
+    if length == 0:
+        return False
+    for j in range(length):
+        polygon = polygons[j]
+        if j == 0:
+            if not is_in_meta_polygon(point, polygon, -0.001):
+                return False
+        else:
+            if is_in_meta_polygon(point, polygon, 0.001):
+                return False
+    return True
+
+try:
+    polygon_list = json.loads(json_poly)
+    total_len = len(polygon_list)
+    point_pair = (float(x), float(y))
+    for index in range(total_len):
+        if is_in_polygon(point_pair, polygon_list[index]):
+            return True
+    return False
+except Exception as e:
+    return False
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 输入
+    1. 点的x坐标
+    2. 点的y坐标
+    3. 业务空间的轮廓
+## 返回结果
+    true    成功
+    false   失败

+ 34 - 13
docs/dev/data-center/relations/other/calc_vertically_overlap.md

@@ -1,21 +1,29 @@
-计算所有业务空间, 在竖直方向上的面积重叠关系
+# 计算各个建筑内不同楼层的业务空间在垂直方向的重叠关系 (业务空间的垂直交通关系?)
 ## 前置条件
-只有有轮廓的业务空间才能参与计算
-```
+    1. 楼层下有业务空间
+    2. 业务空间有轮廓
 
-```
 ## 处理逻辑
+    1. 因为算法复杂度问题, 该算法分为两个版本, 区别为参与计算的业务空间的范围
+    版本一 (参与计算的业务空间 : 项目下全部的业务空间都参与计算):
+        1). 获取项目下所有有楼层关系, 并且有轮廓的业务空间
+        2). 将业务空间按照所属楼层, 业务空间类型分组
+        3). 以一群人两两握手的方式将两两楼层内的相同类型的业务空间做轮廓是否重叠判断
+        4). 如果有重叠, 则认为两业务空间有垂直方向的重叠关系
+        5). 删除上次自动计算的关系, 并插入新计算的关系
+    版本二 (参与计算的业务空间 : 指定业务空间类型下的若干个业务空间 与某一个建筑的内其他楼层的相同类型的业务空间):
+        1). 获取到指定类型的所有业务空间, 按照楼层分类
+        2). 如果某一层下有指定的业务空间, 则该楼层计算与其他楼层的垂直关系时, 那么该楼层只有指定的业务空间才参与计算轮廓重叠
+        3). 则计算结果一定是只与指定的业务空间有关系
+        4). 删除与指定的业务空间相关的关系, 并插入新计算的关系
 
-    1. 获取所有建筑, for循环获取每个建筑下所有的业务空间, 按楼层分类
-    2. 每个楼层的每个业务空间分别和别的楼层的每个业务空间判断is_vertically_overlap
-    3. 将结果是true的两个业务空间保存起来
-    4. 删除旧业务空间的垂直交通关系(自动计算的), 添加新关系
 
-## 实现方式
+# 函数
+## 版本1
 
+<details>
+<summary>版本1 : 计算一个项目内, 所有业务空间类型的业务空间垂直交通关系</summary>
 
-# 函数
-## 版本1 (计算一个项目内, 所有业务空间类型的业务空间垂直交通关系)
 ```
 CREATE OR REPLACE FUNCTION "public"."is_vertically_overlap"("project_id" varchar)
   RETURNS "pg_catalog"."bool" AS $BODY$
@@ -159,9 +167,15 @@ $BODY$
   LANGUAGE plpython3u VOLATILE
   COST 100
 ```
+</details>
+
+
 
 ## 版本2
-    计算一个建筑内, 同一业务空间类型下的某几个业务空间的垂直交通关系
+<details>
+<summary>版本2 : 计算一个建筑内, 同一业务空间类型下的某几个业务空间的垂直交通关系</summary>
+
+    
 ```
 CREATE OR REPLACE FUNCTION "public"."is_vertically_overlap"("project_id" varchar, "_building_id" varchar, "zone_type" varchar, "space_ids" _varchar)
   RETURNS "pg_catalog"."bool" AS $BODY$
@@ -325,9 +339,16 @@ $BODY$
   LANGUAGE plpython3u VOLATILE
   COST 100
 ```
+</details>
 
 ## 输入
-    1. 项目id
+    1. 版本一
+        1). 项目id
+    2. 版本二
+        1). 项目id
+        2). 建筑id
+        3). 业务空间类型
+        4). 指定参与计算的业务空间id
 ## 返回结果
     true    成功
     false   失败

+ 6 - 0
docs/dev/data-center/relations/other/other_ispace_floor_overlap.md

@@ -8,6 +8,10 @@
     将楼层的外轮廓与元空间的最外层轮廓做是否重叠检查操作
 
 # 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 CREATE OR REPLACE FUNCTION "public"."other_ispace_floor_overlap"("ispace_outline" jsonb, "floor_outline" jsonb)
   RETURNS "pg_catalog"."bool" AS $BODY$
@@ -46,6 +50,8 @@ $BODY$
 
 ```
 
+</details>
+
 ## 输入
     1. 元空间轮廓
     2. 楼层轮廓

+ 5 - 5
docs/dev/data-center/relations/other/rel_affected_space.md

@@ -4,8 +4,6 @@
     2. 划分了业务空间
     3. 该模型上传时, 该楼层上必须已有模型
     4. 原模型有元空间信息
-```
-```
 
 ## 依赖函数
@@ -25,13 +23,13 @@
     4. 根据2, 3中改变的元空间来判断 1中获取的关系中哪些业务空间因元空间的改变而发生了改变
     5. 返回4中得到的业务空间
 
-## 实现方式
 
-```
-```
 
 # 函数
 
+<details>
+<summary>源码</summary>
+
 ```plpython
 CREATE OR REPLACE FUNCTION rel_affected_space(floor_id character varying) RETURNS boolean AS
 $$
@@ -97,6 +95,8 @@ $$
 LANGUAGE 'plpython3u' VOLATILE;
 ```
 
+</details>
+
 
 ## 入参
     1. web模型服务中的floor_id, 格式是uuid

+ 43 - 0
docs/dev/data-center/relations/other/rel_cilin_str_similar.md

@@ -0,0 +1,43 @@
+# 根据词林相似度算法匹配相似度最高的字符串
+## 前置条件
+    
+    输入合法即可
+    
+## 处理逻辑
+
+[文档](http://adm.sagacloud.cn:8080/doc/dev/data-center/relations/other/sim.html)
+[git地址 : http://git.sagacloud.cn/chenhaiyang/sim](http://git.sagacloud.cn/chenhaiyang/sim)
+
+
+# 函数
+
+<details>
+<summary>源码</summary>
+
+```
+CREATE OR REPLACE FUNCTION "public"."rel_cilin_str_similar"("word" varchar, "possibilities" _varchar, "num" int4)
+  RETURNS "pg_catalog"."text" AS $BODY$
+
+from sim.text_sim import most_similar_items
+
+try:
+    result = most_similar_items(word, possibilities, num)
+    return result
+except Exception as e:
+    plpy.warning(e)
+    return ""
+else:
+    plpy.warning(e)
+$BODY$
+  LANGUAGE plpython3u VOLATILE
+  COST 100
+```
+</details>
+
+## 输入
+    1. word : 需要匹配的字符串
+    2. possibilities : 被匹配的字符串
+    3. num : 指定返回匹配到的字符串中相似度最高的num个字符串
+## 返回结果
+    
+![rel_cilin_1](./img/rel_cilin-1.png)

+ 5 - 5
docs/dev/data-center/relations/other/rel_str_similar.md

@@ -1,4 +1,4 @@
-计算字符的相似度 (使用编辑距离)
+# 计算字符的相似度 (使用编辑距离)
 ## 前置条件
 ```
@@ -10,13 +10,12 @@
 
     根据编辑距离来计算相似度
 
-## 实现方式
-
-```
-```
 
 # 函数
 
+<details>
+<summary>源码</summary>
+
 ```plpython
 CREATE OR REPLACE FUNCTION "public"."rel_str_similar"("word" varchar, "possibilities" _varchar, "num" int4, "sim" float8)
   RETURNS "pg_catalog"."text" AS $BODY$
@@ -69,6 +68,7 @@ $BODY$
   COST 100
 ```
 
+</details>
 
 ## 入参
     1. "word" varchar                    需要匹配的字符串

+ 110 - 0
docs/dev/data-center/relations/other/sim.md

@@ -0,0 +1,110 @@
+# sim-dict
+
+## 简介 Introduction
+
+sim-dict 是一个计算文本相似度的模块,支持词语、短文本、长文本相似度计算;对中文字符提供包含语义的相似度计算,非中文字符只支持字符层面的相似度计算。
+
+## 快速上手 Getting Started
+
+```python
+>>> from sim.text_sim import get_similarity
+>>> get_similarity('环境算法实现', '温度控制策略')
+0.7166666666666667
+```
+
+## 入门 Basics
+
+### 环境准备 Prerequisite
+
+建议使用 [virtualenv](https://virtualenv.pypa.io/en/latest/) 创建虚拟环境作为开发和生产环境,virtualenv 是用来为应用创建一套“隔离”的 Python 运行环境的工具。
+
+首先,用 pip 安装 virtualenv
+
+```bash
+$ pip3 install virtualenv
+```
+
+假如我们要开发一个新的项目,需要一套独立的 Python 运行环境,可以这么做:
+
+第一步,创建目录:
+
+```bash
+~ mkdir myproject
+~ cd myproject
+```
+
+第二步,创建一个独立的 Python 运行环境,命名为 venv:
+
+```bash
+~/myproject virtualenv -p /usr/bin/python venv
+Running virtualenv with interpreter /usr/bin/python
+Already using interpreter /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
+New python executable in /Users/highing/code/python/test/venv/bin/python
+Installing setuptools, pip, wheel...
+done.
+```
+
+参数 p 用来指定创建环境使用的 Python 解释器的路径,比如我们要使用 Python 3.8 版本的虚拟环境,可以用版本为 3.8 的解释器路径来替
+代 -p 后边的参数,这样创建的环境也就是 Python 3.8 了
+
+命令 virtualenv 就可以创建一个独立的 Python 运行环境,新建的 Python 环境就被放到当前目录下的 venv 目录。我们可以通过 source 命令进入环境
+
+```bash
+~/myproject source venv/bin/activate
+(venv) ~/myproject
+```
+
+### 安装 Installation
+
+sim-dict 目前已经被打包放在公司的[私服仓库](http://dev.dp.sagacloud.cn:8082/#admin/repository/repositories)了,我们可以在创建好的虚拟环境安装它
+
+```bash
+(venv) ~/myproject pip install short-text-sim -i http://dev.dp.sagacloud.cn:8082/repository/saga-pypi-group/simple --trusted-host dev.dp.sagacloud.cn
+```
+
+上述命令可以加上 --upgrade 参数完成更新操作。
+
+## 进阶篇 Advanced
+
+在 sim-dict 实现的文本相似度计算方法中,大概分为分词、词相似度计算、文本相似度计算三个步骤。
+
+### 关于词林
+
+[「同义词词林」](https://baike.baidu.com/item/%E5%90%8C%E4%B9%89%E8%AF%8D%E8%AF%8D%E6%9E%97)是梅家驹等人与 1983 年词林编撰完成的,后经哈工大更新扩展,sim-dict 是在扩展版基础上实现的。
+词林根据词语的含义、类型、词性将所收录的汉语词分为了 12 大类,从大到小一共五层,第五层成为原子词群,意为不可再分,每个原子词群都有一个五层结构的编码,比如 Aa01C02=。
+
+### 词相似度计算
+
+根据词林建立一个「词——编码」的哈希表,通过这样的哈希表,就可以查询词的编码。获取两个待计算相似度词的编码,对比两个编码,根据匹配程度计算相似度。编码具有五个层级,
+从大到小匹配得分分别为 3,3,2,1,1,总分为 10 分,匹配累计得分与总分的比值即为相似度。
+
+### 文本相似度计算
+
+ 将两个待计算相似度的文本进行分词操作,我们这里用的分词工具是[结巴分词](https://github.com/fxsjy/jieba)。根据分词结构计算词相似度,得到如下的词相似度矩阵
+ 分别获取该矩阵每行、每列的最大值的均值,计算这两个平均值的均值,即为这两个文本的相似度。
+
+ -   | 环境 | 算法 | 实现
+ ----| ----| --- | ---
+ 温度 | 0.4 | 0.3 | 0.2
+ 控制 | 0.1 | 0.3 | 0.4
+ 策略 | 0.4 | 0.6 | 0.2
+
+### 更新词林
+
+往词林是添加新词(词林未收录的词)没有明确统一的方法,主要基于个人对于词林对理解,下边是我们常用的思路。
+
+对于一个新词,尝试列举它的同义词、近义词,如果它的同/近义词在词林里,那么将该新词直接放在它的同/近义词所在的原子词群即可,这样通过词林计算二者相似度的时候即可得到最高的分数,
+与我们认为的二者是同义/近义是相符的。
+
+如果无法列举该词合适的同/近义词,或者该词的同/近义词未被词林收录,我们要尝试总结该词的类型、词性、词义,并根据词林分类的逻辑,将其放入合适的词群或者建立新的词群和与之对应的编码。
+比如“空调主机”要放在第一大类“物”里,第二大类“机具”里,第三类“机器 工具 泵”里……
+
+如果该词是多义的,对于每一个词义,按照上述逻辑分别执行添词操作即可。
+
+由于基于词林方法的性质,即通过人的理解将词分类,并以此为基础进行词、句相似度的计算,所以目前来说无法通过机器自动完成新词添加操作,这也是这种方法维护最为繁琐的地方。
+
+## 附录 Appendix
+
+[基于同义词词林的词语相似度计算方法](http://gb.oversea.cnki.net/KCMS/detail/detail.aspx?filename=CCYD201006010&dbcode=CJFD&dbname=CJFD2010)
+
+[Final_word_Similarity](https://github.com/yaleimeng/Final_word_Similarity)

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


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

@@ -0,0 +1,60 @@
+### 下面列出所有问题, 每个问题需要标注问题解决人, 问题类型
+
+## 模型web服务历史和现存问题
+### 已解决
+#### 一般问题
+    1. 回退模型版本时出现异常Bug(邢景灏)
+    2. 连接件的关系不能支持1对多(邢景灏)
+    3. 模型文件上传不显示正在上传楼层的进度(邢景灏, 张宇)
+        解决: 由传统上传方式改为分片上传文件
+    4. 轴网检查, 标高检查结果异常(邢景灏)
+    5. 模型删除的通知缺失(邢景灏)
+    6. 模型导出之后没有元空间数据(李浩然)
+    7. 模型上传一半失败的情况下不能替换模型(邢景灏, 张宇)
+    8. 模型版本在2以后不再增长问题(邢景灏)
+        
+#### 性能问题
+    1. 通过接口同步数据到数据中心在数据量较大时容易失败(邢景灏)
+        解决: 改为多次小批量同步数据
+    2. 模型导出的json接口, 功能聚合度过高, 处理时间过长, 容易引发接口请求超时的问题(邢景灏)
+        解决: 接口功能拆分, 遵守接口的单一职责原则
+    
+## 数据中心计算历史和现存问题
+### 已解决
+    1. 模型对象外轮廓信息不标准导致受影响的业务空间计算异常(邢景灏)
+    2. 由关系表迁移和楼层ModelId引起的多个算法计算失败(邢景灏)
+### <font color=red>未解决</font>
+    1. 管网计算分块逻辑有问题 (邢景灏)
+        暂未详细查找原因
+    2. 因为暂时未找到虚拟墙的合适使用方式, 在计算业务空间之间的关系(部分)时, 只要使用到了虚拟墙, 目前忽略.(邢景灏)
+         
+        
+## 调度器历史和现存问题
+### 已解决
+#### 性能问题
+    1. 派发任务效率较低 (邢景灏)
+    2. 任务返回结果数据量过大, 导致全局数据操作变慢(邢景灏)
+
+#### 架构问题
+    1. 在tcp连接时, 线上环境偶尔出现假死状态, 客户端和服务器不能正常通信(邢景灏)
+    2. 在tcp长连接断网时需要添加自动重连机制, 否则需要人工介入重启服务(邢景灏)
+    3. 由基于tcp/ip的CS架构切换为http接口调用方式, 服务器端的接口和结构调整(邢景灏)
+    4. 客户端在切换处理对象(服务器端)时, 因为id格式和后台限制原因不能正常工作(邢景灏)
+
+#### 架构问题
+    1. 由基于tcp/ip的CS架构切换为http接口调用方式, 客户端的调整(庞利祥)
+    2. 服务器端未与新客户端完成对接 (庞利祥, 邢景灏)
+    
+
+
+## 模型检查和导出客户端问题
+
+### 已解决
+    1. 缺少软管, 软水管数据, 导致管网计算不能正常计算 (孟向歌)
+    2. 模型的导出数据有残缺, 出现过多个不同的bug(孟向歌)
+    3. 模型检查结果异常, 出现过多个不同的bug(孟向歌)
+    4. 因为网络不稳定导致下载模型文件失败, 用户经常需要重新上传模型(邢景灏)
+### <font color=red>未解决</font>
+    1. 模型检查报告异常, 有错误没检查出来 (孟向歌)
+    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)
+

+ 20 - 13
docs/dev/data-center/relations/topology/sys_block.md

@@ -1,23 +1,27 @@
-管网系统设备分块
+# 管网系统设备分块
 ## 前置条件
-1. 项目下的模型文件已上传;
-2. 所有管网已正确连接;
-3. 相邻层立管开放端口坐标位置相等(距离差值不能超过0.01)
-4. (非强制)通过模型检查:连接件检查、系统类型名称检查、管网及相关设备检查、管段检查
+    1. 项目下的模型文件已上传;
+    2. 所有管网已正确连接;
+    3. 相邻层立管开放端口坐标位置相等(距离差值不能超过0.01)
+    4. (非强制)通过模型检查:连接件检查、系统类型名称检查、管网及相关设备检查、管段检查
 
 ## 处理逻辑
 
-1. 获取所有当前项目project_id下的的有模型文件models; 表:revit.model_folder、revit.model_floor、revit.model_file
-2. 遍历models,获取model层的系统图systemGraphs;
-    1. 获取model下且domain、systemName符合条件的connector,connectors,表:revit.connector
-    2. 获取model下所有的设备、部件、其它、风管、水管,表:revit.equipment、revit.component、revit.other、revit.pipe、revit.duct
-    3. 获取model层的系统图数据,存在systemGraphs中。
-3. 获取systemGraphs中所有的开放立管,按开放立管对连接的系统图systemGraphs进行合并;
-4. 删除本项目下project_id、指定系统类型systemName,指定域domain,所有数据,表revit_calc.connected_block
-4. 将合并后的数据存入数据库,表:revit_calc.connected_block。
+    1. 获取所有当前项目project_id下的的有模型文件models; 表:revit.model_folder、revit.model_floor、revit.model_file
+    2. 遍历models,获取model层的系统图systemGraphs;
+        1. 获取model下且domain、systemName符合条件的connector,connectors,表:revit.connector
+        2. 获取model下所有的设备、部件、其它、风管、水管,表:revit.equipment、revit.component、revit.other、revit.pipe、revit.duct
+        3. 获取model层的系统图数据,存在systemGraphs中。
+    3. 获取systemGraphs中所有的开放立管,按开放立管对连接的系统图systemGraphs进行合并;
+    4. 删除本项目下project_id、指定系统类型systemName,指定域domain,所有数据,表revit_calc.connected_block
+    4. 将合并后的数据存入数据库,表:revit_calc.connected_block。
 
 
 # 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 create or replace function public.sys_block(project_id character varying,building_id character varying,system_name character varying,domain character varying) returns boolean
 as
@@ -130,6 +134,9 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.sys_block('Pj1101010015','Bd11010100153c05821ed8fd11e9b8f2d79d0a5b4bf6','冷冻水供水管','DomainPiping')
 ```
 
+</details>
+
+
 ## 输入
     1. 项目id
     2. 需要计算的building_id,如果没有提供时请传入null,不能传""

+ 13 - 7
docs/dev/data-center/relations/topology/sys_direction.md

@@ -1,18 +1,22 @@
-管网系统确定流向
+# 管网系统确定流向
 ## 前置条件
 
-1. 通过管网系统分块计算;
-2. 指定源。
+    1. 通过管网系统分块计算;
+    2. 指定源。
 
 ## 处理逻辑
 
-1. 从connected_block表中取出所有关系,构建无向图
-2. 从connected_block_source表中取出所有的源
-3. 由图算法确定流向,构建有方向的图
-4. 更新connected_block表
+    1. 从connected_block表中取出所有关系,构建无向图
+    2. 从connected_block_source表中取出所有的源
+    3. 由图算法确定流向,构建有方向的图
+    4. 更新connected_block表
 
 
 # 函数
+
+<details>
+<summary>源码</summary>
+
 ```
 create or replace function public.sys_direction(project_id character varying,building_id character varying,block_id character varying, system_name character varying,domain character varying,is_source boolean default true) returns boolean
 as
@@ -72,6 +76,8 @@ LANGUAGE 'plpython3u' VOLATILE;
 select public.sys_direction('Pj1101010015','Bd11010100153c05821ed8fd11e9b8f2d79d0a5b4bf6','0','冷冻水供水管','DomainPiping',true)
 ```
 
+</details>
+
 ## 输入
     1. 项目id
     2. 需要计算的building_id,如果没有提供时请传入null,不能传""

+ 10 - 0
docs/dev/saga-graphy/floor-map/mapDemo.md

@@ -0,0 +1,10 @@
+# 可编辑直线示例
+::: details 目录
+[[toc]]
+:::
+
+<example-web-graph-MapDemo/>
+
+::: details 查看代码
+<<< @/docs/.vuepress/components/example/web/graph/mapDemo.vue
+:::

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

@@ -1,5 +1,10 @@
 # 颜色与样式
+
 ::: details 目录
 [[toc]]
 :::
 
+## 阴影
+
+<example-web-graph-style-shadow /> 
+

+ 5 - 2
docs/dev/saga-graphy/index.js

@@ -26,11 +26,13 @@ const content = [
                     ["/dev/saga-graphy/scene-manage/items/text", "文本"],
                     ["/dev/saga-graphy/scene-manage/items/image", "图片"],
                     ["/dev/saga-graphy/scene-manage/items/editLine", "可编辑直线"],
-                    ["/dev/saga-graphy/scene-manage/items/edit-polygon", "可编辑多边形示例"],
+                    ["/dev/saga-graphy/scene-manage/items/editPolyline", "可编辑折线"],
+                    ["/dev/saga-graphy/scene-manage/items/editPolygon", "可编辑多边形示例"],
                     ["/dev/saga-graphy/scene-manage/items/imgText", "图例item(图片文本组合)"]
                 ]
             },
-            ["/dev/saga-graphy/scene-manage/undo", "Undo示例"]
+            ["/dev/saga-graphy/scene-manage/undo", "Undo示例"],
+            ["/dev/saga-graphy/scene-manage/align", "对齐示例"],
         ]
     },
     {
@@ -41,6 +43,7 @@ const content = [
             ["/dev/saga-graphy/floor-map/downloadFile", "手工下载楼层底图文件"],
             ["/dev/saga-graphy/floor-map/jsonFile", "json数据格式"],
             ["/dev/saga-graphy/floor-map/divide", "划分"],
+            ["/dev/saga-graphy/floor-map/mapDemo", "楼层平面图范例"],
         ]
     },
     {

+ 16 - 0
docs/dev/saga-graphy/scene-manage/align.md

@@ -0,0 +1,16 @@
+# 对齐示例
+
+::: details 目录
+[[toc]]
+:::
+
+<example-web-graph-scene-Align />
+
+::: details 查看代码
+<<< @/docs/.vuepress/components/example/web/graph/scene/Align.vue
+:::
+
+::: tip 注
+1、使用对齐时,至少选择2个item<br/>
+2、使用分散对齐时,至少选择3个item<br/>
+:::

+ 2 - 4
docs/dev/saga-graphy/scene-manage/items/editLine.md

@@ -6,10 +6,8 @@
 <example-web-graph-scene-SEditLine/>
 
 ::: tip 注
-1、可以通过双击table在编辑状态和展示状态来回切换,<br/>
-2、创建状态时;enter + 左键闭合多边形 <br/>
-3、编辑状态时:鼠标点击在边上可加点,可拖动点;<br/>
-4、编辑状态时:通过ALT键 + 鼠标左键 删除顶点
+1、可以通过双击直线在编辑状态和展示状态来回切换,<br/>
+2、编辑状态时:鼠标点击在直线端点上,可拖动点;<br/>
 :::
 
 ::: details 查看代码

docs/dev/saga-graphy/scene-manage/items/edit-polygon.md → docs/dev/saga-graphy/scene-manage/items/editPolygon.md


+ 15 - 0
docs/dev/saga-graphy/scene-manage/items/editPolyline.md

@@ -0,0 +1,15 @@
+# 可编辑折线示例
+
+<example-web-graph-scene-PolylineItem/>
+
+::: tip 注
+1、enter完成创建 <br/>
+2、双击折线附近,进入编辑态;再次双击进入查看态 <br/>
+3、编辑态时:点击点附近更改点位置 <br/>
+4、编辑状态时:通过ALT键 + 鼠标左键 删除顶点
+:::
+
+
+::: details 查看代码
+<<< @/docs/.vuepress/components/example/web/graph/scene/PolylineItem.vue
+:::

+ 1 - 1
docs/dev/saga-graphy/scene-manage/items/image.md

@@ -7,4 +7,4 @@
 
 ::: details 查看代码
 <<< @/docs/.vuepress/components/example/web/graph/scene/ImageItem.vue
-:::
+:::

+ 12 - 3
docs/dev/saga-graphy/system-diagram/json-file.md

@@ -9,7 +9,7 @@
 {
     Data:[
         {
-            EntityList:[],
+          
             Elements:{                          // 系统图展示所需数据
                 Nodes:[],                       // 图例节点,所有与工程信息化相关的图例(图标类型与区域)
                 Markers:[],                     // 与工程信息无关的标识对象(增加文本注释,图上的图片说明)
@@ -21,8 +21,17 @@
             ProjectID: '',                      // 项目ID
             BuildingID: '',                     // 建筑ID
             FloorID: '',                        // 楼层id
-            Note: ''                           // 图说明
-            
+            Note: '',                           // 图说明
+            Log: {                              //图操作日志
+                  Mark:'',                      //图的存盘标记
+                  CommandList:[
+                   {
+                        Command: '',            //命令
+                        Desc: '',               //描述
+                        Detail: ''              //详情
+                    }
+                ]
+            }
         }
     ],
     Message: '',

+ 4 - 4
package.json

@@ -12,10 +12,10 @@
     "publish": "node publish.js"
   },
   "dependencies": {
-    "@saga-web/base": "^2.1.9",
-    "@saga-web/big": "^1.0.24",
-    "@saga-web/draw": "^2.1.84",
-    "@saga-web/graph": "^2.1.68",
+    "@saga-web/base": "2.1.16",
+    "@saga-web/big": "1.0.35",
+    "@saga-web/draw": "2.1.90",
+    "@saga-web/graph": "2.1.78",
     "@saga-web/feng-map": "1.0.6",
     "axios": "^0.18.1",
     "element-ui": "^2.12.0",