|
@@ -31,14 +31,26 @@ public class DanReportParseService {
|
|
|
PDFTextStripper stripper = new PDFTextStripper();
|
|
PDFTextStripper stripper = new PDFTextStripper();
|
|
|
stripper.setSortByPosition(true);
|
|
stripper.setSortByPosition(true);
|
|
|
String fullText = stripper.getText(document);
|
|
String fullText = stripper.getText(document);
|
|
|
- return parseText(fullText, dimension);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if ("adolescent".equals(dimension)) {
|
|
|
|
|
+ java.awt.image.BufferedImage compassImage = extractB5CompassImage(document);
|
|
|
|
|
+ return parseText(fullText, dimension, compassImage);
|
|
|
|
|
+ }
|
|
|
|
|
+ return parseText(fullText, dimension, null);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 从纯文本解析 DAN 报告
|
|
|
|
|
|
|
+ * 从纯文本解析 DAN 报告(无图像版本)
|
|
|
*/
|
|
*/
|
|
|
public DanParsedReport parseText(String fullText, String dimension) {
|
|
public DanParsedReport parseText(String fullText, String dimension) {
|
|
|
|
|
+ return parseText(fullText, dimension, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从纯文本解析 DAN 报告(含可选图像用于B5指南针分析)
|
|
|
|
|
+ */
|
|
|
|
|
+ private DanParsedReport parseText(String fullText, String dimension, java.awt.image.BufferedImage compassImage) {
|
|
|
String[] lines = fullText.split("\\r?\\n");
|
|
String[] lines = fullText.split("\\r?\\n");
|
|
|
List<String> lineList = new ArrayList<>();
|
|
List<String> lineList = new ArrayList<>();
|
|
|
for (String line : lines) {
|
|
for (String line : lines) {
|
|
@@ -67,7 +79,7 @@ public class DanReportParseService {
|
|
|
} else if ("campus".equals(dimension)) {
|
|
} else if ("campus".equals(dimension)) {
|
|
|
return parseC1Report(lineList, fullText);
|
|
return parseC1Report(lineList, fullText);
|
|
|
} else if ("adolescent".equals(dimension)) {
|
|
} else if ("adolescent".equals(dimension)) {
|
|
|
- return parseB5Report(lineList, fullText);
|
|
|
|
|
|
|
+ return parseB5Report(lineList, fullText, compassImage);
|
|
|
}
|
|
}
|
|
|
return DanParsedReport.empty();
|
|
return DanParsedReport.empty();
|
|
|
}
|
|
}
|
|
@@ -845,7 +857,7 @@ public class DanReportParseService {
|
|
|
* - 运动数据:时长、强度
|
|
* - 运动数据:时长、强度
|
|
|
* - 网络依赖数据
|
|
* - 网络依赖数据
|
|
|
*/
|
|
*/
|
|
|
- private DanParsedReport parseB5Report(List<String> lines, String fullText) {
|
|
|
|
|
|
|
+ private DanParsedReport parseB5Report(List<String> lines, String fullText, java.awt.image.BufferedImage compassImage) {
|
|
|
List<DataItem> items = new ArrayList<>();
|
|
List<DataItem> items = new ArrayList<>();
|
|
|
Map<String, Object> extra = new LinkedHashMap<>();
|
|
Map<String, Object> extra = new LinkedHashMap<>();
|
|
|
StringBuilder summary = new StringBuilder();
|
|
StringBuilder summary = new StringBuilder();
|
|
@@ -985,6 +997,23 @@ public class DanReportParseService {
|
|
|
extra.put("internetDuration", inM.group(1));
|
|
extra.put("internetDuration", inM.group(1));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 7. 人际关系指南针图19个子维度分数(第9页图像分析)
|
|
|
|
|
+ if (compassImage != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Map<String, Double> compassScores = extractB5CompassScores(compassImage, fullText);
|
|
|
|
|
+ for (Map.Entry<String, Double> entry : compassScores.entrySet()) {
|
|
|
|
|
+ items.add(new DataItem(
|
|
|
|
|
+ entry.getKey().replace('_', '.'),
|
|
|
|
|
+ "人际指南针_" + entry.getKey(),
|
|
|
|
|
+ String.valueOf(entry.getValue()),
|
|
|
|
|
+ "compass"));
|
|
|
|
|
+ extra.put(entry.getKey(), entry.getValue());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("B5指南针图像提取失败,跳过: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 通用正则提取
|
|
// 通用正则提取
|
|
|
Pattern scorePattern = Pattern.compile("([\\u4e00-\\u9fa5]{2,8})[::]\\s*(\\d+(\\.\\d+)?)");
|
|
Pattern scorePattern = Pattern.compile("([\\u4e00-\\u9fa5]{2,8})[::]\\s*(\\d+(\\.\\d+)?)");
|
|
|
Matcher matcher = scorePattern.matcher(fullText);
|
|
Matcher matcher = scorePattern.matcher(fullText);
|
|
@@ -1409,6 +1438,177 @@ public class DanReportParseService {
|
|
|
return "";
|
|
return "";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ======================== B5 指南针图像分析常量 ========================
|
|
|
|
|
+
|
|
|
|
|
+ /** 象限角度范围 (图像坐标系: 0°=右, 90°=下, 180°=左, 270°=上) */
|
|
|
|
|
+ private static final Map<String, int[]> B5_QUAD_ANGLES = new LinkedHashMap<String, int[]>() {{
|
|
|
|
|
+ put("母亲关系", new int[]{185, 265});
|
|
|
|
|
+ put("父亲关系", new int[]{140, 178});
|
|
|
|
|
+ put("师生关系", new int[]{275, 350});
|
|
|
|
|
+ put("同伴关系", new int[]{2, 42});
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ /** 每度fill到分数的线性映射系数 */
|
|
|
|
|
+ private static final Map<String, Double> B5_QUAD_FACTORS = new LinkedHashMap<String, Double>() {{
|
|
|
|
|
+ put("母亲关系", 0.006974);
|
|
|
|
|
+ put("父亲关系", 0.005834);
|
|
|
|
|
+ put("师生关系", 0.006820);
|
|
|
|
|
+ put("同伴关系", 0.005049);
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ /** 指南针中心坐标 */
|
|
|
|
|
+ private static final int B5_CENTER_X = 857;
|
|
|
|
|
+ private static final int B5_CENTER_Y = 758;
|
|
|
|
|
+
|
|
|
|
|
+ /** 各象限的子维度名称 */
|
|
|
|
|
+ private static final Map<String, String[]> B5_SUB_DIMS = new LinkedHashMap<String, String[]>() {{
|
|
|
|
|
+ put("母亲关系", new String[]{"控制压迫", "情感疏离", "价值冲突", "双重标准", "过度期待"});
|
|
|
|
|
+ put("父亲关系", new String[]{"控制压迫", "情感疏离", "价值冲突", "双重标准", "过度期待"});
|
|
|
|
|
+ put("师生关系", new String[]{"回避型", "敌对型", "高控制型", "关系疏离型"});
|
|
|
|
|
+ put("同伴关系", new String[]{"回避型", "被排斥型", "攻击型", "边缘型", "特殊因素型"});
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== B5 指南针图像分析方法 ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从B5 PDF文档第9页提取人际关系指南针图像
|
|
|
|
|
+ * 返回PNG图像解析出的BufferedImage,不含数据则表示未找到图像
|
|
|
|
|
+ */
|
|
|
|
|
+ private java.awt.image.BufferedImage extractB5CompassImage(PDDocument document) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (document.getNumberOfPages() < 9) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ org.apache.pdfbox.pdmodel.PDPage page = document.getPage(8);
|
|
|
|
|
+ org.apache.pdfbox.pdmodel.PDResources resources = page.getResources();
|
|
|
|
|
+ if (resources == null) return null;
|
|
|
|
|
+
|
|
|
|
|
+ int imgIndex = 0;
|
|
|
|
|
+ for (org.apache.pdfbox.cos.COSName name : resources.getXObjectNames()) {
|
|
|
|
|
+ org.apache.pdfbox.pdmodel.graphics.PDXObject xobj = resources.getXObject(name);
|
|
|
|
|
+ if (xobj instanceof org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) {
|
|
|
|
|
+ if (imgIndex == 3) {
|
|
|
|
|
+ org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject img =
|
|
|
|
|
+ (org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) xobj;
|
|
|
|
|
+ return img.getImage();
|
|
|
|
|
+ }
|
|
|
|
|
+ imgIndex++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("B5指南针图像提取失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 沿指定角度测量填充范围(像素半径)
|
|
|
|
|
+ * 从r=150开始扫描跳过中心色块, 25px间隙容忍用于跨越填充分区内的环线
|
|
|
|
|
+ */
|
|
|
|
|
+ private Integer measureFillAtAngle(java.awt.image.BufferedImage img, int cx, int cy, double angleDeg) {
|
|
|
|
|
+ double angle = Math.toRadians(angleDeg);
|
|
|
|
|
+ double cosA = Math.cos(angle);
|
|
|
|
|
+ double sinA = Math.sin(angle);
|
|
|
|
|
+ int w = img.getWidth();
|
|
|
|
|
+ int h = img.getHeight();
|
|
|
|
|
+ int maxR = Math.min(Math.min(w - cx, cx), Math.min(cy, h - cy)) - 2;
|
|
|
|
|
+ Integer fillEnd = null;
|
|
|
|
|
+ int gap = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (int r = 150; r < maxR; r++) {
|
|
|
|
|
+ int x = cx + (int)(r * cosA);
|
|
|
|
|
+ int y = cy + (int)(r * sinA);
|
|
|
|
|
+ if (x < 0 || x >= w || y < 0 || y >= h) break;
|
|
|
|
|
+
|
|
|
|
|
+ int rgb = img.getRGB(x, y);
|
|
|
|
|
+ int red = (rgb >> 16) & 0xFF;
|
|
|
|
|
+ int green = (rgb >> 8) & 0xFF;
|
|
|
|
|
+ int blue = rgb & 0xFF;
|
|
|
|
|
+
|
|
|
|
|
+ boolean isWhite = red > 230 && green > 230 && blue > 230;
|
|
|
|
|
+ boolean isGray = red > 155 && red < 195 && green > 155 && green < 195 && blue > 155 && blue < 195;
|
|
|
|
|
+ boolean isBlack = red < 20 && green < 20 && blue < 20;
|
|
|
|
|
+ boolean isColored = !(isWhite || isGray || isBlack);
|
|
|
|
|
+
|
|
|
|
|
+ if (isColored) {
|
|
|
|
|
+ fillEnd = r;
|
|
|
|
|
+ gap = 0;
|
|
|
|
|
+ } else if (fillEnd != null) {
|
|
|
|
|
+ gap++;
|
|
|
|
|
+ if (gap > 25) break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return fillEnd;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从B5指南针图像提取19个子维度分数
|
|
|
|
|
+ * 返回 { "人际_母亲关系_控制压迫": score, ... }
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Double> extractB5CompassScores(
|
|
|
|
|
+ java.awt.image.BufferedImage img, String fullText) {
|
|
|
|
|
+ Map<String, Double> result = new LinkedHashMap<>();
|
|
|
|
|
+ if (img == null) return result;
|
|
|
|
|
+
|
|
|
|
|
+ int cx = B5_CENTER_X;
|
|
|
|
|
+ int cy = B5_CENTER_Y;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算每象限的per-student校准因子
|
|
|
|
|
+ Map<String, Double> quadFactors = new LinkedHashMap<>();
|
|
|
|
|
+ for (Map.Entry<String, int[]> entry : B5_QUAD_ANGLES.entrySet()) {
|
|
|
|
|
+ String qname = entry.getKey();
|
|
|
|
|
+ int a1 = entry.getValue()[0];
|
|
|
|
|
+ int a2 = entry.getValue()[1];
|
|
|
|
|
+
|
|
|
|
|
+ List<Integer> fills = new ArrayList<>();
|
|
|
|
|
+ for (int a = a1; a <= a2; a++) {
|
|
|
|
|
+ Integer fe = measureFillAtAngle(img, cx, cy, a);
|
|
|
|
|
+ if (fe != null) fills.add(fe);
|
|
|
|
|
+ }
|
|
|
|
|
+ double avg = fills.isEmpty() ? 1.0
|
|
|
|
|
+ : fills.stream().mapToInt(Integer::intValue).average().orElse(1.0);
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试从第9页文本提取关系维度的总分作为校准基准
|
|
|
|
|
+ double txtScore = 3.0;
|
|
|
|
|
+ Pattern p = Pattern.compile(Pattern.quote(qname) + "\\s*\\n\\s*(\\d+(?:\\.\\d+)?)");
|
|
|
|
|
+ Matcher m = p.matcher(fullText);
|
|
|
|
|
+ if (m.find()) {
|
|
|
|
|
+ try { txtScore = Double.parseDouble(m.group(1)); } catch (NumberFormatException ignored) {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ double factor = avg > 0 ? txtScore / avg : B5_QUAD_FACTORS.getOrDefault(qname, 0.005);
|
|
|
|
|
+ quadFactors.put(qname, factor);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提取各象限的子维度分数
|
|
|
|
|
+ for (Map.Entry<String, int[]> entry : B5_QUAD_ANGLES.entrySet()) {
|
|
|
|
|
+ String quadName = entry.getKey();
|
|
|
|
|
+ int a1 = entry.getValue()[0];
|
|
|
|
|
+ int a2 = entry.getValue()[1];
|
|
|
|
|
+ String[] dims = B5_SUB_DIMS.get(quadName);
|
|
|
|
|
+ if (dims == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ int n = dims.length;
|
|
|
|
|
+ double step = (a2 - a1) / (double)(n + 1);
|
|
|
|
|
+ double factor = quadFactors.getOrDefault(quadName,
|
|
|
|
|
+ B5_QUAD_FACTORS.getOrDefault(quadName, 0.005));
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
|
|
+ double angle = a1 + (i + 1) * step;
|
|
|
|
|
+ List<Integer> fills = new ArrayList<>();
|
|
|
|
|
+ for (int offset = -2; offset <= 2; offset += 2) {
|
|
|
|
|
+ Integer fe = measureFillAtAngle(img, cx, cy, angle + offset);
|
|
|
|
|
+ if (fe != null) fills.add(fe);
|
|
|
|
|
+ }
|
|
|
|
|
+ double avgFill = fills.isEmpty() ? 0
|
|
|
|
|
+ : fills.stream().mapToInt(Integer::intValue).average().orElse(0);
|
|
|
|
|
+ double score = Math.min(5.0, Math.max(1.0, Math.round(avgFill * factor * 10.0) / 10.0));
|
|
|
|
|
+ result.put("人际_" + quadName + "_" + dims[i], score);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ======================== 内部数据类 ========================
|
|
// ======================== 内部数据类 ========================
|
|
|
|
|
|
|
|
/**
|
|
/**
|