package com.guo.learningprogresstracker.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import java.util.*;
/**
* 思维导图树结构与缩进大纲文本之间的双向转换工具。
*
大纲格式:每行一条节点,缩进表示层级(2 空格 / tab / # 前缀),行首 - * 数字. 等标记被自动剥离。
*/
public class MindMapTreeTool {
private static final int INDENT_SPACES = 2;
private MindMapTreeTool() {
}
// ============ 序列化:树 → 缩进大纲 ============
/**
* 将根节点序列化为缩进大纲文本
*/
public static String toOutline(MindMapNode root) {
StringBuilder sb = new StringBuilder();
appendOutline(sb, root, 0);
return sb.toString();
}
private static void appendOutline(StringBuilder sb, MindMapNode node, int level) {
if (level > 0) {
sb.append(" ".repeat(level)).append("- ").append(node.getTitle() != null ? node.getTitle() : "").append('\n');
}
if (node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
appendOutline(sb, child, level + 1);
}
}
// level 0 是根节点标题本身不输出,但其子节点输出
if (level == 0 && node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
appendOutline(sb, child, 0);
}
}
}
/**
* 根节点序列化为完整大纲,第一行为根标题
*/
public static String toFullOutline(MindMapNode root) {
StringBuilder sb = new StringBuilder();
sb.append(root.getTitle() != null ? root.getTitle() : "").append('\n');
if (root.getChildren() != null) {
for (MindMapNode child : root.getChildren()) {
appendOutline(sb, child, 1);
}
}
return sb.toString();
}
// ============ 反序列化:缩进大纲 → 树 ============
/**
* 将缩进大纲文本解析为根节点。
* 第一行非空文本作为根标题,后续行作为子节点。
*/
public static MindMapNode parseOutline(String outlineText) {
if (outlineText == null || outlineText.isBlank()) {
return new MindMapNode("未命名");
}
String[] lines = outlineText.split("\\R");
List nonBlank = new ArrayList<>();
for (String line : lines) {
if (!line.trim().isBlank()) {
nonBlank.add(line.stripTrailing());
}
}
if (nonBlank.isEmpty()) {
return new MindMapNode("未命名");
}
MindMapNode root = new MindMapNode(stripMarker(nonBlank.get(0).strip()));
List roots = new ArrayList<>();
Deque stack = new ArrayDeque<>();
stack.push(new StackEntry(-1, roots));
for (int i = 1; i < nonBlank.size(); i++) {
String raw = nonBlank.get(i);
int level = detectLevel(raw);
String title = stripMarker(raw.strip());
MindMapNode child = new MindMapNode(title);
while (stack.peek().level >= level) {
stack.pop();
}
stack.peek().children.add(child);
List children = new ArrayList<>();
child.setChildren(children);
stack.push(new StackEntry(level, children));
}
root.setChildren(roots);
return root;
}
private static int detectLevel(String line) {
String trimmed = line.stripLeading();
int indent = line.length() - trimmed.length();
int hashCount = 0;
while (hashCount < trimmed.length() && trimmed.charAt(hashCount) == '#') {
hashCount++;
}
if (hashCount > 0) return hashCount;
if (indent == 0) return 1;
return Math.max(1, indent / INDENT_SPACES + 1);
}
private static String stripMarker(String s) {
return s.replaceFirst("^#{1,6}\\s*", "")
.replaceFirst("^[-*+]\\s*", "")
.replaceFirst("^\\d+\\.\\s*", "")
.strip();
}
private record StackEntry(int level, List children) {
}
// ============ 工具方法 ============
/** 展开树为平铺列表(前序遍历) */
public static List flatten(MindMapNode root) {
List list = new ArrayList<>();
flattenRecursive(root, list);
return list;
}
private static void flattenRecursive(MindMapNode node, List acc) {
acc.add(node);
if (node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
flattenRecursive(child, acc);
}
}
}
/** 统计节点总数 */
public static int countNodes(MindMapNode root) {
return flatten(root).size();
}
/** 计算最大深度 */
public static int maxDepth(MindMapNode node) {
if (node.getChildren() == null || node.getChildren().isEmpty()) {
return 1;
}
return 1 + node.getChildren().stream().mapToInt(MindMapTreeTool::maxDepth).max().orElse(0);
}
/** 浅拷贝节点(仅拷贝标量字段,不拷贝 children) */
private static MindMapNode copyNodeShallow(MindMapNode node) {
MindMapNode copy = new MindMapNode();
copy.setTitle(node.getTitle());
copy.setNotes(node.getNotes());
copy.setSourceType(node.getSourceType());
copy.setSourceId(node.getSourceId());
return copy;
}
/**
* 按路径提取子树:以 / 分隔的节点标题路径,提取目标节点为根的新树。
* @param root 完整树根
* @param path 节点路径,如 "根标题 / 分支A / 子节点B"
* @return 以路径末端节点为根的深拷贝子树,匹配失败时返回 null
*/
public static MindMapNode extractSubtree(MindMapNode root, String path) {
if (root == null || path == null || path.isBlank()) return null;
String[] segments = path.split("\\s*/\\s*");
MindMapNode current = root;
for (int i = 0; i < segments.length; i++) {
String seg = segments[i].trim();
if (seg.isEmpty()) continue;
if (current.getTitle() != null && normalizeForMerge(current.getTitle()).equals(normalizeForMerge(seg))) {
continue; // 当前节点已匹配,看下一段
}
MindMapNode found = null;
if (current.getChildren() != null) {
for (MindMapNode child : current.getChildren()) {
if (child.getTitle() != null && normalizeForMerge(child.getTitle()).equals(normalizeForMerge(seg))) {
found = child;
break;
}
}
}
if (found != null) {
current = found;
} else {
return null;
}
}
return deepCopy(current);
}
/** 深拷贝节点及其子树 */
private static MindMapNode deepCopy(MindMapNode node) {
if (node == null) return null;
MindMapNode copy = new MindMapNode();
copy.setTitle(node.getTitle());
copy.setNotes(node.getNotes());
copy.setSourceType(node.getSourceType());
copy.setSourceId(node.getSourceId());
List children = new ArrayList<>();
if (node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
children.add(deepCopy(child));
}
}
copy.setChildren(children);
return copy;
}
/**
* 查找与 content 最相似的节点。基于 title 的 bigram Jaccard 相似度。
* @return 得分最高的节点,若所有节点得分均低于 0.1 则返回 null
*/
public static MindMapNode findClosestNode(MindMapNode root, String content) {
if (root == null || content == null || content.isBlank()) return null;
List flat = flatten(root);
MindMapNode best = null;
double bestScore = 0.0;
for (MindMapNode node : flat) {
if (node.getTitle() == null || node.getTitle().isBlank()) continue;
double score = similarityScore(node.getTitle(), content);
// notes 也参与匹配,但权重减半
if (node.getNotes() != null && !node.getNotes().isBlank()) {
double noteScore = similarityScore(node.getNotes(), content);
score = Math.max(score, noteScore * 0.5);
}
if (score > bestScore) {
bestScore = score;
best = node;
}
}
return bestScore > 0.1 ? best : null;
}
/**
* 获取节点在树中的路径(以 / 分隔的 title 序列)
*/
public static String getPath(MindMapNode root, String targetTitle) {
if (root == null || targetTitle == null) return "";
List path = new ArrayList<>();
if (findPathRecursive(root, targetTitle, path)) {
return String.join(" / ", path);
}
return "";
}
private static boolean findPathRecursive(MindMapNode node, String targetTitle, List path) {
if (node == null) return false;
path.add(node.getTitle() != null ? node.getTitle() : "");
if (normalizeForMerge(node.getTitle()).equals(normalizeForMerge(targetTitle))) return true;
if (node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
if (findPathRecursive(child, targetTitle, path)) return true;
}
}
path.remove(path.size() - 1);
return false;
}
/** bigram Jaccard 相似度 */
public static double similarityScore(String a, String b) {
if (a == null || b == null) return 0;
String na = normalizeForMerge(a);
String nb = normalizeForMerge(b);
if (na.isEmpty() || nb.isEmpty()) return 0;
if (na.equals(nb)) return 1.0;
// 子串匹配
if (na.contains(nb) || nb.contains(na)) return 0.9;
Set bigramsA = bigrams(na);
Set bigramsB = bigrams(nb);
if (bigramsA.isEmpty() && bigramsB.isEmpty()) return 0;
Set union = new HashSet<>(bigramsA);
union.addAll(bigramsB);
Set intersect = new HashSet<>(bigramsA);
intersect.retainAll(bigramsB);
return (double) intersect.size() / union.size();
}
private static Set bigrams(String s) {
Set set = new LinkedHashSet<>();
for (int i = 0; i < s.length() - 1; i++) {
set.add(s.substring(i, i + 2));
}
return set;
}
/**
* 合并新旧树:保留旧树中已有节点(用户编辑),追加新树中的新节点。
* 按标准化标题匹配同级节点,旧树匹配到的节点优先保留。
*
* @param oldRoot 旧树根(用户可能编辑过)
* @param newRoot 新树根(AI/BUILTIN 最新生成)
* @return 合并后的树
*/
public static MindMapNode mergeTrees(MindMapNode oldRoot, MindMapNode newRoot) {
if (oldRoot == null) return newRoot;
if (newRoot == null) return oldRoot;
MindMapNode result = copyNodeShallow(oldRoot);
result.setChildren(mergeChildren(oldRoot.getChildren(), newRoot.getChildren()));
return result;
}
/** 递归合并子节点列表 */
private static List mergeChildren(List oldChildren, List newChildren) {
Map oldByNormalized = new LinkedHashMap<>();
if (oldChildren != null) {
for (MindMapNode child : oldChildren) {
oldByNormalized.put(normalizeForMerge(child.getTitle()), child);
}
}
List merged = new ArrayList<>();
Set usedKeys = new HashSet<>();
if (newChildren != null) {
for (MindMapNode newNode : newChildren) {
String key = normalizeForMerge(newNode.getTitle());
MindMapNode oldNode = oldByNormalized.get(key);
if (oldNode != null) {
// 旧节点存在:保留旧节点标题,递归合并子节点
MindMapNode kept = copyNodeShallow(oldNode);
kept.setChildren(mergeChildren(oldNode.getChildren(), newNode.getChildren()));
merged.add(kept);
usedKeys.add(key);
} else {
// 新节点:直接添加
merged.add(newNode);
}
}
}
// 添加旧树中有但新树中没有的节点(用户添加的额外节点)
if (oldChildren != null) {
for (MindMapNode oldNode : oldChildren) {
if (!usedKeys.contains(normalizeForMerge(oldNode.getTitle()))) {
merged.add(oldNode);
}
}
}
return merged;
}
private static String normalizeForMerge(String s) {
if (s == null) return "";
return s.replaceAll("[\\s 、,。!?:;()\\[\\]{},.!?:;()\\-—/\\\\|]", "")
.toLowerCase(Locale.ROOT)
.trim();
}
/** 序列化整棵树为 JSON */
public static String toJson(MindMapNode root, ObjectMapper mapper) {
try {
return mapper.writeValueAsString(root.toMap());
} catch (Exception e) {
return "{\"title\":\"序列化失败\"}";
}
}
/** 从 JSON 反序列化 */
public static MindMapNode fromJson(String json, ObjectMapper mapper) {
return MindMapNode.fromJson(json, mapper);
}
}