From 0786fb99a83bfdfc9981074894523aa4748e77f0 Mon Sep 17 00:00:00 2001 From: duwenyuan <15600000461@163.com> Date: Wed, 7 Jan 2026 20:42:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E6=9D=BF=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/DistributionAnalysisToolkit.java | 367 ++++++++++++++++++ .../service/impl/HtmlReportGenerator.java | 179 +++++++++ 2 files changed, 546 insertions(+) create mode 100644 jeecg-module-spectrum-analysis/src/main/java/org/jeecg/common/util/DistributionAnalysisToolkit.java create mode 100644 jeecg-module-spectrum-analysis/src/main/java/org/jeecg/modules/service/impl/HtmlReportGenerator.java diff --git a/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/common/util/DistributionAnalysisToolkit.java b/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/common/util/DistributionAnalysisToolkit.java new file mode 100644 index 00000000..d6ad7b7e --- /dev/null +++ b/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/common/util/DistributionAnalysisToolkit.java @@ -0,0 +1,367 @@ +package org.jeecg.common.util; + +import lombok.Data; +import org.jeecg.modules.entity.vo.NuclideAnalysisInfo; +import org.jeecg.modules.entity.vo.NuclideRatioResult; + +import java.util.*; +import java.util.stream.Collectors; + +public class DistributionAnalysisToolkit { + /** + * 区间统计结果 + */ + @Data + public static class IntervalStat { + private final String interval; + private final int count; + private final List values; + private final Map levelDistribution; + + public IntervalStat(String interval, int count, List values, Map levelDistribution) { + this.interval = interval; + this.count = count; + this.values = values; + this.levelDistribution = levelDistribution; + } + + public IntervalStat(String interval, List nuclideData) { + values = new ArrayList<>(); + levelDistribution = new HashMap<>(); + this.interval = interval; + this.count = nuclideData.size(); + for (NuclideAnalysisInfo nuclideAnalysis : nuclideData) { + values.add(nuclideAnalysis.getConc()); + levelDistribution.merge(nuclideAnalysis.getCategory(), 1, Integer::sum); + } + } + + + public String getInterval() { + return interval; + } + + public int getCount() { + return count; + } + + public List getValues() { + return values; + } + } + + /** + * KDE曲线点 + */ + public static class KDEPoint { + private final double x; + private final double density; + + public KDEPoint(double x, double density) { + this.x = x; + this.density = density; + } + + public double getX() { + return x; + } + + public double getDensity() { + return density; + } + } + + /** + * CDF曲线点 + */ + public static class CDFPoint { + private final double value; + private final double cumulativeProb; + + public CDFPoint(double value, double cumulativeProb) { + this.value = value; + this.cumulativeProb = cumulativeProb; + } + + public double getValue() { + return value; + } + + public double getCumulativeProb() { + return cumulativeProb; + } + } + + /** + * 数据区间统计 + * + * @param data 原始数据 + * @param start 起始值 + * @param step 区间宽度 + * @return 区间统计结果列表 + */ + public static List calculateIntervalStats(List data, double start, double step) { + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + try { + // 计算结束边界 + double max = data.stream() + .mapToDouble(NuclideAnalysisInfo::getConc) + .max().orElse(0); + double end = Math.ceil(max / step) * step + step; + + // 初始化区间映射 + Map> intervalMap = new TreeMap<>(); + for (double lower = start; lower < end; lower += step) { + double upper = lower + step; + String key = String.format("[%.1f, %.1f)", lower, upper); + intervalMap.put(key, new ArrayList<>()); + } + + // 分配数据到区间 + for (NuclideAnalysisInfo nuclide : data) { + try { + double value = nuclide.getConc(); + double lower = Math.floor(value / step) * step; + String key = String.format("[%.1f, %.1f)", lower, lower + step); + intervalMap.get(key).add(nuclide); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + // 转换为统计结果对象 + List stats = new ArrayList<>(); + for (Map.Entry> entry : intervalMap.entrySet()) { + stats.add(new IntervalStat(entry.getKey(), entry.getValue())); + } + + return stats; + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return new ArrayList<>(); + } + + public static List calculateIntervalStats(List data, double start, double step,String ratio) { + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + try { + // 计算结束边界 + double max =data.stream().mapToDouble(Double::doubleValue).max().orElse(0) ; + double end = Math.ceil(max / step) * step + step; + + // 初始化区间映射 + Map> intervalMap = new TreeMap<>(); + for (double lower = start; lower < end; lower += step) { + double upper = lower + step; + String key = String.format("[%.1f, %.1f)", lower, upper); + intervalMap.put(key, new ArrayList<>()); + } + + // 分配数据到区间 + for (double value : data) { + try { + double lower = Math.floor(value / step) * step; + String key = String.format("[%.1f, %.1f)", lower, lower + step); + intervalMap.get(key).add(value); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + // 转换为统计结果对象 + List stats = new ArrayList<>(); + for (Map.Entry> entry : intervalMap.entrySet()) { + stats.add(new IntervalStat(entry.getKey(), entry.getValue().size(),entry.getValue(),new HashMap<>())); + } + + return stats; + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return new ArrayList<>(); + } + + /** + * 计算95%累积线 + * + * @param data 原始数据 + * @return 95%累积线值 + */ + public static double calculate95thPercentile(List data) { + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + + // 排序数据 + List sortedData = new ArrayList<>(data); + Collections.sort(sortedData); + + int n = sortedData.size(); + double position = 0.95 * (n - 1); + + int lowerIndex = (int) Math.floor(position); + int upperIndex = (int) Math.ceil(position); + + if (lowerIndex == upperIndex) { + return sortedData.get(lowerIndex); + } + + // 线性插值 + double lowerValue = sortedData.get(lowerIndex); + double upperValue = sortedData.get(upperIndex); + double fraction = position - lowerIndex; + + return lowerValue + fraction * (upperValue - lowerValue); + } + + + /** + * 计算累积分布函数(CDF) + * + * @param data 原始数据 + * @return CDF点列表 + */ + public static List calculateCDF(List data) { + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + + // 排序数据 + List sortedData = new ArrayList<>(data); + Collections.sort(sortedData); + + // 计算累积分布 + List cdfPoints = new ArrayList<>(); + int n = sortedData.size(); + + for (int i = 0; i < n; i++) { + double value = sortedData.get(i); + double cumulativeProbability = (i + 1.0) / n; + cdfPoints.add(new CDFPoint(value, cumulativeProbability)); + } + + return cdfPoints; + } + + + /** + * 核函数接口 + */ + @FunctionalInterface + public interface KernelFunction { + double apply(double u); + } + + // 常用核函数实现 + public static final KernelFunction GAUSSIAN_KERNEL = u -> Math.exp(-0.5 * u * u) / Math.sqrt(2 * Math.PI); + public static final KernelFunction EPANECHNIKOV_KERNEL = u -> (Math.abs(u) <= 1) ? 0.75 * (1 - u * u) : 0; + public static final KernelFunction TRIANGULAR_KERNEL = u -> (Math.abs(u) <= 1) ? 1 - Math.abs(u) : 0; + + + /** + * 使用Silverman规则计算最佳带宽 + */ + public static double calculateBandwidthSilverman(List data) { + int n = data.size(); + if (n <= 1) return 1.0; + + // 计算标准差 + double mean = data.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); + double variance = data.stream() + .mapToDouble(x -> Math.pow(x - mean, 2)) + .average() + .orElse(0.0); + double sigma = Math.sqrt(variance); + + // 计算四分位距(IQR) + List sortedData = new ArrayList<>(data); + Collections.sort(sortedData); + + double q1 = sortedData.get((int) Math.ceil(0.25 * n - 1)); + double q3 = sortedData.get((int) Math.ceil(0.75 * n - 1)); + double iqr = q3 - q1; + + // Silverman规则 + return 0.9 * Math.min(sigma, iqr / 1.34) * Math.pow(n, -0.2); + } + + /** + * 核密度估计 + */ + public static List kernelDensityEstimate( + List data, KernelFunction kernel, double bandwidth) { + + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + + // 计算数据范围 + double min = Collections.min(data); + double max = Collections.max(data); + double range = max - min; + double minX = min - 0.1 * range; + double maxX = max + 0.1 * range; + + // 生成评估点 + int numPoints = 200; + double step = (maxX - minX) / (numPoints - 1); + + List kdePoints = new ArrayList<>(); + int n = data.size(); + + for (int i = 0; i < numPoints; i++) { + double x = minX + i * step; + double sum = 0.0; + + for (double value : data) { + double u = (x - value) / bandwidth; + sum += kernel.apply(u); + } + + double density = sum / (n * bandwidth); + kdePoints.add(new KDEPoint(x, density)); + } + + return kdePoints; + } + + /** + * 自动KDE计算(自动选择带宽) + */ + public static List autoKDE(List data, KernelFunction kernel) { + double bandwidth = calculateBandwidthSilverman(data); + return kernelDensityEstimate(data, kernel, bandwidth); + } + + + //获取浓度值集合 + public static List convertConcToDoubleList(List nuclideList) { + return nuclideList.stream() // 创建流 + .map(NuclideAnalysisInfo::getConc) // 提取conc值 + .collect(Collectors.toList()); // 收集为List + } + + public static List convertRatioToDoubleList(List nuclideList) { + return nuclideList.stream() // 创建流 + .map(NuclideRatioResult::getRatio) // Ratio + .collect(Collectors.toList()); // 收集为List + } + + //累积和 + public static List cumulativeSum(List data) { + List result = new ArrayList<>(); + double sum = 0.0; + + for (double value : data) { + sum += value; + result.add(sum); + } + + return result; + } + +} diff --git a/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/modules/service/impl/HtmlReportGenerator.java b/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/modules/service/impl/HtmlReportGenerator.java new file mode 100644 index 00000000..597140dd --- /dev/null +++ b/jeecg-module-spectrum-analysis/src/main/java/org/jeecg/modules/service/impl/HtmlReportGenerator.java @@ -0,0 +1,179 @@ +package org.jeecg.modules.service.impl; + +import org.jeecg.modules.service.IGenerateHtmlReport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +@Service +public class HtmlReportGenerator implements IGenerateHtmlReport { + + @Autowired + private TemplateEngine templateEngine; + + /** + * 构造方法,初始化Thymeleaf模板引擎 + * 默认从resources/templates/目录加载HTML模板 + */ + public void generatorInit() { + ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setPrefix("/templates/"); + templateResolver.setSuffix(".html"); + templateResolver.setTemplateMode(TemplateMode.HTML); + templateResolver.setCharacterEncoding("UTF-8"); + this.templateEngine.setTemplateResolver(templateResolver); + } + + /** + * 生成基础HTML报告 + * @param data 报告数据,键值对形式 + * @return 生成的HTML字符串 + */ + @Override + public String generateBasicHtmlReport(Map data) { + Context context = new Context(); + context.setVariables(data); + return templateEngine.process("report", context); + } + + /** + * 生成带自定义样式的HTML报告 + * @param data 报告数据,键值对形式 + * @param style 自定义CSS样式字符串 + * @return 生成的HTML字符串 + */ + @Override + public String generateStyledHtmlReport(Map data, String style) { + Context context = new Context(); + context.setVariables(data); + context.setVariable("customStyle", style); + return templateEngine.process("styled-report", context); + } + + /** + * 生成HTML报告并保存到文件 + * @param data 报告数据,键值对形式 + * @param filePath 文件保存路径(包含文件名) + * @return 保存成功返回true,失败返回false + */ + @Override + public boolean generateHtmlReportToFile(Map data, String filePath) { + try { + String html = generateBasicHtmlReport(data); + Files.write(Paths.get(filePath), html.getBytes(StandardCharsets.UTF_8)); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 使用指定模板生成HTML报告 + * @param data 报告数据,键值对形式 + * @param templatePath 模板文件路径(相对于classpath) + * @return 生成的HTML字符串 + */ + @Override + public String generateHtmlReportWithTemplate(Map data, String templatePath) { + ClassLoaderTemplateResolver tempResolver = new ClassLoaderTemplateResolver(); + tempResolver.setPrefix("/"); + tempResolver.setSuffix(""); + tempResolver.setTemplateMode(TemplateMode.HTML); + + TemplateEngine tempEngine = new TemplateEngine(); + tempEngine.setTemplateResolver(tempResolver); + + Context context = new Context(); + context.setVariables(data); + + return tempEngine.process(templatePath, context); + } + + /** + * 生成高级HTML报告(可选包含图表和交互功能) + * @param data 报告数据,键值对形式 + * @param includeCharts 是否包含图表(需要data中包含chartData) + * @param interactive 是否添加交互功能 + * @return 生成的HTML字符串 + */ + @Override + public String generateAdvancedHtmlReport(Map data, boolean includeCharts, boolean interactive) { + Context context = new Context(); + context.setVariables(data); + context.setVariable("includeCharts", includeCharts); + context.setVariable("interactive", interactive); + return templateEngine.process("advanced-report", context); + } + + /** + * 生成多页HTML报告 + * @param pagesData 多页数据,Key为页面名称,Value为页面数据 + * @return 生成的HTML字符串 + */ + @Override + public String generateMultiPageHtmlReport(Map> pagesData) { + Context context = new Context(); + context.setVariable("pages", pagesData); + return templateEngine.process("multi-page-report", context); + } + + /** + * 生成HTML报告字节数组(便于网络传输) + * @param data 报告数据,键值对形式 + * @return UTF-8编码的HTML字节数组 + */ + @Override + public byte[] generateHtmlReportAsBytes(Map data) { + return generateBasicHtmlReport(data).getBytes(StandardCharsets.UTF_8); + } + + /** + * 生成分页HTML报告 + * @param data 数据列表,每个元素为一组键值对 + * @param itemsPerPage 每页显示的项目数 + * @return 生成的HTML字符串(包含分页导航) + */ + @Override + public String generatePaginatedHtmlReport(List> data, int itemsPerPage) { + Context context = new Context(); + int totalPages = (int) Math.ceil((double) data.size() / itemsPerPage); + context.setVariable("totalPages", totalPages); + context.setVariable("itemsPerPage", itemsPerPage); + context.setVariable("data", data); + return templateEngine.process("paginated-report", context); + } + + /** + * 生成响应式HTML报告(适配移动设备) + * @param data 报告数据,键值对形式 + * @return 生成的HTML字符串(包含响应式CSS) + */ + @Override + public String generateResponsiveHtmlReport(Map data) { + Context context = new Context(); + context.setVariables(data); + return templateEngine.process("responsive-report", context); + } + + /** + * 生成Base64编码的HTML报告(便于嵌入其他系统) + * @param data 报告数据,键值对形式 + * @return Base64编码的HTML字符串 + */ + @Override + public String generateHtmlReportAsBase64(Map data) { + return Base64.getEncoder().encodeToString(generateBasicHtmlReport(data).getBytes(StandardCharsets.UTF_8)); + } +}