262 lines
11 KiB
Java
262 lines
11 KiB
Java
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;
|
||
}
|
||
}
|
||
}
|
||
}
|