Files
flow-center-service/src/main/java/com/yunhai/flowcenter/service/DiagramLayoutService.java
T

262 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.model.FlowModels;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 流程图布局单源(2026-10-03 三视图统一;2026-10-09 竖排改造):拓扑最长路径分层
* (层=行,自上向下),层内按定义顺序横排、整层块对竖直中线水平居中;并行分支组以
* SPLIT 竖直中线为轴左右对称展开(SPLIT↔JOIN 配对按各出边可达集求交、取 BFS 序最早的
* JOIN);输出内容区坐标(minX/minY 已归零),PNG 渲染与 DiagramVo.positions 共同消费——
* 改布局只改这里。
*/
@Service
public class DiagramLayoutService {
/** 逻辑坐标常量(盒型在槽内居中;层间距留给竖直走线与多出口文案,分支列间无需走线空间) */
static final int SLOT_H = 100; // 层槽高(盒型在槽内垂直居中)
static final int GAP_Y = 56; // 层间距(竖直段 + 多出口文案空间)
static final int SLOT_W = 280; // 分支槽宽(盒型在槽内水平居中)
static final int GAP_X = 44; // 分支列间距
/** 各类型盒型(w,h):开始/结束胶囊、审批圆角卡(名单 2 行)、并行/条件菱形(取外接矩形) */
static int[] boxOf(String type) {
switch (type == null ? "" : type) {
case FlowDefModel.TYPE_START:
case FlowDefModel.TYPE_END:
return new int[]{116, 44};
case FlowDefModel.TYPE_PARALLEL_SPLIT:
case FlowDefModel.TYPE_PARALLEL_JOIN:
case FlowDefModel.TYPE_CONDITION:
return new int[]{132, 78};
default:
return new int[]{264, 76};
}
}
/** nodes+edges → nodeId→{x,y,w,h}(内容区坐标,minX/minY 已归零)。PNG 与 DiagramVo.positions 共同消费;改布局只改这里。 */
public Map<String, int[]> layout(List<FlowModels.NodeVo> nodes, List<FlowModels.EdgeVo> edges) {
Map<String, FlowModels.NodeVo> byId = new HashMap<>();
for (FlowModels.NodeVo n : nodes) {
byId.put(n.getId(), n);
}
Map<String, List<String>> out = new HashMap<>();
Map<String, Integer> inDeg = new LinkedHashMap<>();
for (FlowModels.NodeVo n : nodes) {
inDeg.put(n.getId(), 0);
}
for (FlowModels.EdgeVo e : edges) {
if (byId.containsKey(e.getFrom()) && byId.containsKey(e.getTo())) {
out.computeIfAbsent(e.getFrom(), k -> new ArrayList<>()).add(e.getTo());
inDeg.merge(e.getTo(), 1, Integer::sum);
}
}
// 拓扑最长路径分层(层=行,自上向下);环兜底:无入度的从层 0 起算,成环残留节点强制层 0(仍必出图)
Map<String, Integer> level = new HashMap<>();
List<String> queue = new ArrayList<>();
for (Map.Entry<String, Integer> en : inDeg.entrySet()) {
if (en.getValue() == 0) {
level.put(en.getKey(), 0);
queue.add(en.getKey());
}
}
while (!queue.isEmpty()) {
String id = queue.remove(queue.size() - 1);
int parent = level.get(id);
for (String to : out.getOrDefault(id, new ArrayList<>())) {
level.merge(to, parent + 1, Math::max);
int d = inDeg.merge(to, -1, Integer::sum);
if (d == 0) {
queue.add(to);
}
}
}
for (FlowModels.NodeVo n : nodes) {
level.putIfAbsent(n.getId(), 0);
}
// 初始网格:行 y 由层号决定,层内按定义顺序横排、整层块以内容区竖直中线居中
Map<Integer, List<FlowModels.NodeVo>> layers = new LinkedHashMap<>();
int maxCols = 1;
for (FlowModels.NodeVo n : nodes) {
int l = level.get(n.getId());
layers.computeIfAbsent(l, k -> new ArrayList<>()).add(n);
}
for (List<FlowModels.NodeVo> layer : layers.values()) {
maxCols = Math.max(maxCols, layer.size());
}
int axisW = (maxCols - 1) * (SLOT_W + GAP_X) + SLOT_W;
Map<String, int[]> box = new HashMap<>();
for (Map.Entry<Integer, List<FlowModels.NodeVo>> en : layers.entrySet()) {
List<FlowModels.NodeVo> layer = en.getValue();
int blockW = (layer.size() - 1) * (SLOT_W + GAP_X) + SLOT_W;
int left0 = (axisW - blockW) / 2;
for (int i = 0; i < layer.size(); i++) {
FlowModels.NodeVo n = layer.get(i);
int[] wh = boxOf(n.getType());
box.put(n.getId(), new int[]{
left0 + i * (SLOT_W + GAP_X) + (SLOT_W - wh[0]) / 2,
en.getKey() * (SLOT_H + GAP_Y) + (SLOT_H - wh[1]) / 2,
wh[0], wh[1]});
}
}
// 并行分支组以 SPLIT 竖直中线为轴左右居中(内层组先处理,最后整体推回左边界 0)
centerParallelGroups(nodes, out, box);
// 归零(2026-10-03 三视图统一新增):全体平移使 minX=minY=0,positions 消费端无需再算包围盒
if (!box.isEmpty()) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
for (int[] b : box.values()) {
minX = Math.min(minX, b[0]);
minY = Math.min(minY, b[1]);
}
if (minX != 0 || minY != 0) {
for (int[] b : box.values()) {
b[0] -= minX;
b[1] -= minY;
}
}
}
return box;
}
/**
* 并行分支组水平居中:分支不再从 SPLIT 行向右堆叠,而是以 SPLIT 竖直中线为轴左右对称展开。
* SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的 PARALLEL_JOIN);
* 组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 水平中点吸附回 SPLIT 中线。
* 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最左列推回 0。
*/
private void centerParallelGroups(List<FlowModels.NodeVo> nodes, Map<String, List<String>> out,
Map<String, int[]> box) {
class Group {
final String split;
final String join;
final List<String> members;
final int maxY;
Group(String split, String join, List<String> members, int maxY) {
this.split = split;
this.join = join;
this.members = members;
this.maxY = maxY;
}
}
List<Group> groups = new ArrayList<>();
for (FlowModels.NodeVo split : nodes) {
if (!FlowDefModel.TYPE_PARALLEL_SPLIT.equals(split.getType()) || !box.containsKey(split.getId())) {
continue;
}
String sid = split.getId();
// BFS 序 + 各分支可达集,用于定位配对 JOIN
Map<String, Integer> order = new HashMap<>();
order.put(sid, 0);
List<String> bfs = new ArrayList<>();
bfs.add(sid);
while (!bfs.isEmpty()) {
String cur = bfs.remove(0);
for (String to : out.getOrDefault(cur, new ArrayList<>())) {
if (!order.containsKey(to)) {
order.put(to, order.size());
bfs.add(to);
}
}
}
List<String> heads = out.getOrDefault(sid, new ArrayList<>());
List<java.util.Set<String>> reachSets = new ArrayList<>();
for (String head : heads) {
java.util.Set<String> set = new java.util.HashSet<>();
List<String> q = new ArrayList<>();
q.add(head);
while (!q.isEmpty()) {
String cur = q.remove(0);
if (set.contains(cur)) {
continue;
}
set.add(cur);
q.addAll(out.getOrDefault(cur, new ArrayList<>()));
}
reachSets.add(set);
}
java.util.Set<String> common = reachSets.isEmpty() ? new java.util.HashSet<>() : reachSets.get(0);
for (java.util.Set<String> set : reachSets) {
common = new java.util.HashSet<>(common);
common.retainAll(set);
}
String join = null;
int joinOrder = Integer.MAX_VALUE;
for (FlowModels.NodeVo n : nodes) {
if (FlowDefModel.TYPE_PARALLEL_JOIN.equals(n.getType()) && box.containsKey(n.getId())
&& common.contains(n.getId())) {
int o = order.getOrDefault(n.getId(), Integer.MAX_VALUE);
if (o < joinOrder) {
joinOrder = o;
join = n.getId();
}
}
}
if (join == null) {
continue;
}
// 组员:SPLIT 起步、不穿过 JOIN 的可达集(含嵌套 SPLIT/JOIN)
List<String> members = new ArrayList<>();
java.util.Set<String> seen = new java.util.HashSet<>();
seen.add(join);
List<String> q = new ArrayList<>();
q.add(sid);
while (!q.isEmpty()) {
String cur = q.remove(0);
for (String to : out.getOrDefault(cur, new ArrayList<>())) {
if (seen.contains(to) || !box.containsKey(to)) {
continue;
}
seen.add(to);
members.add(to);
q.add(to);
}
}
int maxLevel = 0;
for (String m : members) {
maxLevel = Math.max(maxLevel, box.get(m)[1]);
}
groups.add(new Group(sid, join, members, maxLevel));
}
// maxY 降序 = 内层组先处理
groups.sort((a, b) -> Integer.compare(b.maxY, a.maxY));
for (Group gp : groups) {
int[] s = box.get(gp.split);
int axisX = s[0] + s[2] / 2;
int left = Integer.MAX_VALUE;
int right = Integer.MIN_VALUE;
for (String m : gp.members) {
int[] b = box.get(m);
left = Math.min(left, b[0]);
right = Math.max(right, b[0] + b[2]);
}
int delta = axisX - (left + right) / 2;
for (String m : gp.members) {
box.get(m)[0] += delta;
}
int[] j = box.get(gp.join);
j[0] += axisX - (j[0] + j[2] / 2);
}
// 左移出界(x<0)的组把整图平移回 0(同 delta 平移不破坏各组居中)
int minX = Integer.MAX_VALUE;
for (int[] b : box.values()) {
minX = Math.min(minX, b[0]);
}
if (minX < 0) {
for (int[] b : box.values()) {
b[0] -= minX;
}
}
}
}