节点位置分布算法

This commit is contained in:
qinoy 2023-05-16 18:17:38 +08:00
parent c008985d94
commit 016a3eca2b
5 changed files with 399 additions and 15 deletions

View File

@ -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<Node> 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<Node> 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<Integer> getRearNodeIndex(int nodeIndex){
List<Integer> 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<Integer> 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<Node> 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();
}
}

View File

@ -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<Node> 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<Node> 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);

View File

@ -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]);
}
}
}

View File

@ -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);
}