diff --git a/com.actionsoft.apps.coe.method.process.subprocess/lib/com.actionsoft.apps.coe.method.process.subprocess.jar b/com.actionsoft.apps.coe.method.process.subprocess/lib/com.actionsoft.apps.coe.method.process.subprocess.jar index f19625b6..31fbefdb 100644 Binary files a/com.actionsoft.apps.coe.method.process.subprocess/lib/com.actionsoft.apps.coe.method.process.subprocess.jar and b/com.actionsoft.apps.coe.method.process.subprocess/lib/com.actionsoft.apps.coe.method.process.subprocess.jar differ diff --git a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphLayout.java b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphLayout.java new file mode 100644 index 00000000..b4a6672a --- /dev/null +++ b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphLayout.java @@ -0,0 +1,184 @@ +package com.actionsoft.apps.coe.method.process.subprocess.graph; + +import com.actionsoft.apps.coe.method.process.subprocess.mode.Node; + +import java.util.*; + +/** + * 图模型节点布局 + * 依托需求定制 算法 + * @author oYang + * @create 2023-05-16 15:55 + */ +public class GraphLayout { + + private final int[][] adjMatrix; // 邻接矩阵 + private final List nodeList; // 节点集合 + private final boolean[] isPosition; // 节点位置是否确定标记 + private double canvasWidth; // 画布宽度 + private double canvasHeight; // 画布高度 + private final double vertInterval; // 垂直间隔 + private final double horizInterval; // 水平间隔 + private final double[][] position; // 节点坐标 + private final double shapeW; // 图形节点的宽度 + private final double shapeH; // 图形节点的高度 + + + public GraphLayout(int[][] adjMatrix, List nodeList) { + this.adjMatrix = adjMatrix; + this.nodeList = nodeList; + this.isPosition = new boolean[nodeList.size()]; + + this.vertInterval = 50.0; + this.horizInterval = 80.0; + + this.shapeW = 100.0; + this.shapeH = 70.0; + + this.position = new double[nodeList.size()][2]; + } + + /** + * 判断当前节点是否存在后置流程 + * @param nodeIndex 当前节点在集合中的索引 + * @return + */ + private boolean existOutLink(int nodeIndex){ + boolean flag = false; + for (int i = 0; i < adjMatrix[nodeIndex].length; i++) { + if (adjMatrix[nodeIndex][i] == 1){ + flag = true; + break; + } + } + return flag; + } + + /** + * 计算当前节点存在几个后置流程 + * @param nodeIndex + * @return + */ + private int countOutLinks(int nodeIndex){ + int num = 0; + for (int i = 0; i < adjMatrix[nodeIndex].length; i++) { + if (adjMatrix[nodeIndex][i] == 1){ + num++; + } + } + return num; + } + + /** + * 获取当前节点的所有后置节点的索引 + * @param nodeIndex + * @return + */ + private List getRearNodeIndex(int nodeIndex){ + List nodeIndexSet = new ArrayList<>(); + for (int i = 0; i < adjMatrix[nodeIndex].length; i++) { + if (adjMatrix[nodeIndex][i] == 1){ + nodeIndexSet.add(i); + } + } + return nodeIndexSet; + } + + public double getCanvasWidth(){ + return canvasWidth; + } + + public double getCanvasHeight(){ + return canvasHeight; + } + + /** + * 节点布局 + */ + public double[][] layOut(){ + double tempY = 0.0; // 记录第一列的高度 + for (int i = 0; i < nodeList.size(); i++) { + // 第一个节点直接放到画布的左上角 + if (i == 0) { + position[i][0] = 100.0; + position[i][1] = 100.0; + tempY = 100.0; // 更新第一列的高度 + isPosition[i] = true; + if (existOutLink(i)){ // 如果存在后置节点 并且后置节点还未渲染 + calculationRearNodePosition(i); + } + continue; + } + if (!isPosition[i]){ + position[i][0] = 100.0; + position[i][1] = tempY + shapeH + vertInterval; + tempY = position[i][1]; // 更新第一列的高度 + // 存在后置节点 + if (existOutLink(i)){ + calculationRearNodePosition(i); + } + } + } + // 确定画布的宽度与高度 + double w = Arrays.stream(position).mapToDouble(position -> position[0]).max().getAsDouble(); + double h = Arrays.stream(position).mapToDouble(position -> position[1]).max().getAsDouble(); + + this.canvasWidth = w + 200.0; + this.canvasHeight = h + 200.0; + + // 打印节点坐标与画布大小 + System.out.printf("画布(%.2f, %.2f)", canvasWidth, canvasHeight); + for (int i = 0; i < position.length; i++) { + System.out.printf("坐标(%.2f, %.2f)", position[i][0], position[i][1]); + } + + return position; + } + + /** + * 计算当前节点所有后置节点的位置 + * @param nodeIndex + */ + private void calculationRearNodePosition(int nodeIndex){ + // 获取后置节点的索引 + List rearNodeIndexSet = getRearNodeIndex(nodeIndex); + for (int i = 0; i < rearNodeIndexSet.size(); i++) { + int rearNodeIndex = rearNodeIndexSet.get(i).intValue(); + if (!isPosition[rearNodeIndex]) { + position[rearNodeIndex][0] = position[nodeIndex][0] + shapeW + horizInterval; // 上一个节点的x坐标 + 图形宽度 + 间隔 = 当前节点的x坐标 + if (i == 0) { + position[rearNodeIndex][1] = position[nodeIndex][1]; // 上一个节点的y坐标 与 第一个后置节点的y坐标一致 + }else { + position[rearNodeIndex][1] = position[nodeIndex][1] + shapeH + i * vertInterval; // 非第一个后置节点的y坐标 = 上一个节点的y坐标 + 后置节点索引 * 垂直间隔 + } + isPosition[rearNodeIndex] = true; + + if (existOutLink(rearNodeIndex)) { + calculationRearNodePosition(rearNodeIndex); + } + } + } + } + + + public static void main(String[] args) { + // 生成邻接矩阵 + int[][] matrix = { + {0,1,0,0}, + {0,0,1,0}, + {0,0,0,0}, + {1,0,0,0}, + }; + ArrayList nodes = new ArrayList<>(4); + Node node1 = new Node(""); + Node node2 = new Node(""); + Node node3 = new Node(""); + Node node4 = new Node(""); + nodes.add(node1); + nodes.add(node2); + nodes.add(node3); + nodes.add(node4); + GraphLayout graphLayout = new GraphLayout(matrix, nodes); + graphLayout.layOut(); + } +} diff --git a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphRender.java b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphRender.java index 216d5495..7d6804b4 100644 --- a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphRender.java +++ b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/GraphRender.java @@ -1,16 +1,21 @@ package com.actionsoft.apps.coe.method.process.subprocess.graph; import com.actionsoft.apps.coe.method.process.subprocess.constant.SubProcessConst; +import com.actionsoft.apps.coe.method.process.subprocess.mode.Node; +import com.actionsoft.apps.coe.pal.pal.repository.cache.PALRepositoryCache; import com.actionsoft.apps.coe.pal.pal.repository.designer.manage.CoeDesignerAPIManager; import com.actionsoft.apps.coe.pal.pal.repository.designer.model.BaseModel; import com.actionsoft.apps.coe.pal.pal.repository.designer.util.CoeDesignerUtil; import com.actionsoft.apps.coe.pal.pal.repository.designer.util.ShapeUtil; +import com.actionsoft.apps.coe.pal.pal.repository.model.PALRepositoryModel; import com.actionsoft.bpms.util.UUIDGener; import com.alibaba.fastjson.JSONObject; +import java.util.List; + public class GraphRender { - private final int numVertex; + private final List nodeList; private final double width; // 画布宽度 private final double height; // 画布高度 private final double shapeInterval = 80.0; // 图形节点在画布上的间隔 @@ -20,10 +25,10 @@ public class GraphRender { private final String modelId; - public GraphRender(int numVertex, String modelId, String definition) { - this.numVertex = numVertex; - this.width = numVertex * (shapeInterval + nodeW); - this.height = numVertex * (shapeInterval + nodeH); + public GraphRender(List nodeList, String modelId, String definition, double width, double height) { + this.nodeList = nodeList; + this.width = width; + this.height = height; this.definition = definition; this.modelId = modelId; @@ -35,13 +40,15 @@ public class GraphRender { page.put("width", width); page.put("height", height); JSONObject elements = defineJsonObj.getJSONObject("elements"); - for (int i = 0; i < numVertex; i++) { + for (int i = 0; i < nodeList.size(); i++) { + PALRepositoryModel repositoryModel = PALRepositoryCache.getCache().get(nodeList.get(i).getId()); JSONObject subProcessNode = ShapeUtil.getProcessShapeDefinition(SubProcessConst.SUB_PROCESS_METHOD_ID, "子流程"); String nodeId = UUIDGener.getObjectId(); subProcessNode.put("id", nodeId); JSONObject subProcessNodeProps = subProcessNode.getJSONObject("props"); subProcessNodeProps.put("x", position[i][0]); subProcessNodeProps.put("y", position[i][1]); + subProcessNode.put("text", repositoryModel.getName()); elements.put(nodeId, subProcessNode); } defineJsonObj.put("elements",elements); diff --git a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/PageRankLayout.java b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/PageRankLayout.java new file mode 100644 index 00000000..d06687cd --- /dev/null +++ b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/graph/PageRankLayout.java @@ -0,0 +1,197 @@ +package com.actionsoft.apps.coe.method.process.subprocess.graph; + +import java.util.Arrays; + +/** + * 图模型节点布局 + * 采用 PageRank 算法 + * @author oYang + * @create 2023-05-16 11:10 + */ +public class PageRankLayout { + + private int[][] matrix; // 邻接矩阵 + private double width; // 画布宽度 + private double height; // 画布高度 + private int maxIter; // 最大迭代次数 + private double damp; // 阻尼因子 + private int nodeSize; // 节点数 + private double[] pageRankVal; // PageRank数据 + private final double shapeInterval = 80.0; // 图形节点在画布上的间隔 + private final double nodeW = 100.0; // 图形节点默认宽度 + private final double nodeH = 70.0; // 图形节点默认高度 + + public PageRankLayout(int[][] matrix, int maxIter) { + this.matrix = matrix; + this.maxIter = maxIter; + this.damp = 0.85; + this.nodeSize = matrix.length; + this.pageRankVal = new double[nodeSize]; + + this.width = matrix.length * (shapeInterval + nodeW); + this.height = matrix.length * (shapeInterval + nodeH); + + // 初始化每个节点的 PageRank 值为 1/N + Arrays.fill(pageRankVal, 1.0/nodeSize); + } + + /** + * 计算每个节点的入度加权平均值 + */ + public void calculatePageRankVal(){ + for (int iter = 0; iter < maxIter; iter++) { + double[] nextPR = new double[nodeSize]; + for (int i = 0; i < nodeSize; i++) { + double sum = 0.0; + for (int j = 0; j < nodeSize; j++) { + if (matrix[j][i] == 1) { + sum += pageRankVal[j] / countOutLinks(j); + } + } + nextPR[i] = (1 - damp) / nodeSize + damp * sum; + } + pageRankVal = nextPR; + } + // 输出结果 + for (int i = 0; i < nodeSize; i++) { + System.out.printf("Node %d: PageRank = %.3f\n", i, pageRankVal[i]); + } + } + + + /** + * 根据节点邻接矩阵计算节点的出度 + * @param nodeIndex + * @return + */ + private int countOutLinks(int nodeIndex){ + int count = 0; + for (int i = 0; i < matrix[nodeIndex].length; i++) { + if (matrix[nodeIndex][i] == 1) { + count++; + } + } + return count; + } + + /** + * 根据节点邻接矩阵计算节点入度 + * @param nodeIndex + * @return + */ + private int countInputLinks(int nodeIndex){ + int count = 0; + for (int i = 0; i < matrix.length; i++) { + if (matrix[i][nodeIndex] == 1){ + count++; + } + } + return count; + } + + private void layOut(){ + int n = pageRankVal.length; + // 计算 PageRank 值最大的节点,将该节点定位在画布的中心 + int maxIndex = 0; + double maxPageRank = pageRankVal[0]; + for (int i = 1; i < n; i++) { + if (pageRankVal[i] > maxPageRank) { + maxIndex = i; + maxPageRank = pageRankVal[i]; + } + } + double centerX = width / 2; + double centerY = height / 2; + + // 计算其它节点相对于最大节点的 PageRank 值的比例关系,以及节点数量的平方根 + double scale = Math.sqrt(n); + double[] distX = new double[n]; + double[] distY = new double[n]; + for (int i = 0; i < n; i++) { + if (i == maxIndex) { + distX[i] = 0; + distY[i] = 0; + } else { + double ratio = pageRankVal[i] / pageRankVal[maxIndex]; + distX[i] = ratio * Math.cos(2 * Math.PI * i / n); + distY[i] = ratio * Math.sin(2 * Math.PI * i / n); + } + } + + // 将节点的 PageRank 值乘以比例关系和节点数量的平方根,得到节点在横向和竖向上的缩放比例 + double[] scaleX = new double[n]; + double[] scaleY = new double[n]; + for (int i = 0; i < n; i++) { + scaleX[i] = distX[i] * scale; + scaleY[i] = distY[i] * scale; + } + + // 将横向和竖向上的缩放比例分别乘以画布的宽度和高度,得到节点在画布上的实际坐标 + double[] x = new double[n]; + double[] y = new double[n]; + for (int i = 0; i < n; i++) { + x[i] = centerX + (int)(scaleX[i] * 200); + y[i] = centerY + (int)(scaleY[i] * 200); + } + + for (int i = 0; i < nodeSize; i++) { + System.out.printf("NodeIndex %d: Position [%f%n,%f%n]", i, x[i], y[i]); + } + } + + /** + * 根据节点 PageRank 值计算节点坐标 + * + * @param pageRank 节点 PageRank 值 + * @param canvasWidth 画布宽度 + * @param canvasHeight 画布高度 + * @return 节点坐标数组,每个点都是 (x, y) 形式 + */ + public double[][] computeNodeCoordinates() { + int n = pageRankVal.length; + double[][] nodeCoords = new double[n][2]; + + double maxPageRank = Double.NEGATIVE_INFINITY; + for (double x : pageRankVal) { + maxPageRank = Math.max(maxPageRank, x); + } + + double centerX = width / 2.0; + double centerY = height / 2.0; + double radius = Math.min(centerX, centerY) - 50; // 保留一些边距避免节点被切掉 + for (int i = 0; i < n; i++) { + double angle = 2 * Math.PI * i / n; + double x = centerX + (pageRankVal[i] / maxPageRank) * radius * Math.cos(angle); + double y = centerY + (pageRankVal[i] / maxPageRank) * radius * Math.sin(angle); + nodeCoords[i][0] = x; + nodeCoords[i][1] = y; + } + + return nodeCoords; + } + + + public static void main(String[] args) { + // 生成邻接矩阵 + int[][] matrix = { + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + }; + PageRankLayout pageRankLayout = new PageRankLayout(matrix, 100); + pageRankLayout.calculatePageRankVal(); + double[][] nodeCoords = pageRankLayout.computeNodeCoordinates(); + + // 输出每个节点在直角坐标系中的坐标 + for (int i = 0; i < nodeCoords.length; i++) { + System.out.printf("(%.2f, %.2f)\n", nodeCoords[i][0], nodeCoords[i][1]); + } + } +} diff --git a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/web/SubProcessWeb.java b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/web/SubProcessWeb.java index 471c79b1..2c7163d2 100644 --- a/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/web/SubProcessWeb.java +++ b/com.actionsoft.apps.coe.method.process.subprocess/src/com/actionsoft/apps/coe/method/process/subprocess/web/SubProcessWeb.java @@ -1,10 +1,7 @@ package com.actionsoft.apps.coe.method.process.subprocess.web; import com.actionsoft.apps.coe.method.process.subprocess.constant.SubProcessConst; -import com.actionsoft.apps.coe.method.process.subprocess.graph.ForceDirectedGraphLayout; -import com.actionsoft.apps.coe.method.process.subprocess.graph.GraphAdjMatrix; -import com.actionsoft.apps.coe.method.process.subprocess.graph.GraphRender; -import com.actionsoft.apps.coe.method.process.subprocess.graph.VertexPreHandle; +import com.actionsoft.apps.coe.method.process.subprocess.graph.*; import com.actionsoft.apps.coe.method.process.subprocess.mode.Node; import com.actionsoft.apps.coe.method.process.subprocess.mode.vo.SubProcessTagVo; import com.actionsoft.apps.coe.pal.constant.CoEConstant; @@ -229,12 +226,11 @@ public class SubProcessWeb extends ActionWeb { // 构建有向图邻接矩阵 GraphAdjMatrix graphAdjMatrix = new GraphAdjMatrix(nodeList); graphAdjMatrix.buildAdjMatrix(nodeIndexMap); - // graphAdjMatrix.printAdjMatrix(); + graphAdjMatrix.printAdjMatrix(); // 获取节点分布 - ForceDirectedGraphLayout graphLayout = new ForceDirectedGraphLayout(graphAdjMatrix.getAdjMatrix()); - graphLayout.run(); - double[][] position = graphLayout.getPosition(); + GraphLayout graphLayout = new GraphLayout(graphAdjMatrix.getAdjMatrix(), nodeList); + double[][] position = graphLayout.layOut(); // 新建模型 PALRepositoryModel parentModel = PALRepositoryCache.getCache().get(locationId); @@ -259,7 +255,7 @@ public class SubProcessWeb extends ActionWeb { baseModel.setDefinition(obj.getString("define")); // 节点渲染 - GraphRender graphRender = new GraphRender(nodeList.size(), model.getId(), baseModel.getDefinition()); + GraphRender graphRender = new GraphRender(nodeList, model.getId(), baseModel.getDefinition(), graphLayout.getCanvasWidth(), graphLayout.getCanvasHeight()); graphRender.handleShapeNodeRender(position); }