Prechádzať zdrojové kódy

Merge branch 'refs/heads/feature-chart-lihb-20250829' into test-merge

# Conflicts:
#	blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java
LHB 1 mesiac pred
rodič
commit
6e88daa58f

+ 1 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java

@@ -122,7 +122,7 @@ public class ExcelTabClientImpl implements ExcelTabClient {
             if (isBatchSave == 0) {
                 //------单表PDF保存------
                 Integer type1 = Integer.parseInt(table.getString("type"));
-                TableInfo tableInfo = tableInfoList.stream().findAny().orElse(null);
+                TableInfo tableInfo = tableInfoList.stream().filter(t -> t.getPkeyId().equals(tabIds)).findAny().orElse(null);
                 if (tableInfo != null && tabIds.contains(tableInfo.getPkeyId())) {
                     String bussPDFTrial = excelTabService.getBussPDFTrial(Long.valueOf(tableInfo.getPkeyId()), contractId, id, 0, 0, dto);
                     if (StringUtils.isNotEmpty(bussPDFTrial)) {

+ 17 - 19
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ExcelTabServiceImpl.java

@@ -3871,29 +3871,27 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
                                         }
 
                                         // 特殊处理多选框
-                                        if (myData.contains("http") && myData.contains("aliyuncs")) {
+                                        if (myData.indexOf("http") >= 0 && (myData.indexOf("aliyuncs") >= 0 ||myData.indexOf("183.247.216.148") >= 0||myData.indexOf("xinan1.zos.ctyun.cn") >= 0)) {
                                             InputStream imageIn = CommonUtil.getOSSInputStream(myData);
-                                            byte[] byteNew = new byte[0];
                                             if (imageIn != null) {
-                                                byteNew = IOUtils.toByteArray(imageIn);
+                                                byte[] bytes = CommonUtil.compressImage3(myData);
+                                                // 这里根据实际需求选择图片类型
+                                                int pictureIdx = workbook.addPicture(bytes, 6);
+                                                CreationHelper helper = workbook.getCreationHelper();
+                                                ClientAnchor anchor = helper.createClientAnchor();
+                                                anchor.setCol1(x1); // param1是列号
+                                                anchor.setCol2(x2);
+                                                anchor.setRow1(y1); // param2是行号
+                                                anchor.setRow2(y2); // param2是行号
+                                                //
+                                                Drawing drawing = sheet.createDrawingPatriarch();
+                                                anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
+                                                // 插入图片
+                                                Picture pict = drawing.createPicture(anchor, pictureIdx); // 调整图片占单元格百分比的大小,1.0就是100%
+                                                pict.resize(0.9, 0.9);
+                                                FileUtils.imageOrientation(sheet, anchor, new DataVO(x1 - 1, y1 - 1));
                                             }
 
-                                            byte[] bytes = CommonUtil.compressImage(byteNew);
-
-                                            CreationHelper helper = workbook.getCreationHelper();
-                                            ClientAnchor anchor = helper.createClientAnchor();
-                                            anchor.setCol1(x1); // param1是列号
-                                            anchor.setCol2(x2);
-                                            anchor.setRow1(y1); // param2是行号
-                                            anchor.setRow2(y2); // param2是行号
-
-                                            Drawing<?> drawing = sheet.createDrawingPatriarch();
-                                            anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
-                                            // 插入图片
-                                            Picture picture = drawing.createPicture(anchor, workbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG));
-                                            picture.resize(1, 1);
-                                            FileUtils.imageOrientation(sheet, anchor, new DataVO(x1 - 1, y1 - 1));
-
                                         } else if (myData.equals("1") && data.html().contains("hc-form-checkbox-group")) {
                                             Row row = sheet.getRow(y1 - 1);
                                             if (row != null) {

+ 206 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java

@@ -7,6 +7,8 @@ import cn.hutool.core.util.HashUtil;
 import cn.hutool.log.StaticLog;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -5087,6 +5089,210 @@ public class FormulaServiceImpl extends BaseServiceImpl<FormulaMapper, Formula>
         try {
             Formula formula = fd.getFormula();
             String f = formula.getFormula();
+            //图表公式
+            if(f.contains("chart")){
+                Matcher m = RegexUtils.matcher(FC_REG + "(chart)\\(([^)]+)\\)", f);
+                while (m.find()) {
+                    String[] args = m.group(2).split(",");
+                    //y轴源数据
+                    List<FormData> target = getFormDataByCode(args[0], tec);
+                    //x轴源数据
+                    List<FormData> target1 = getFormDataByCode(args[1], tec);
+
+                    List<Double> x = new ArrayList<>();
+                    List<Double> y = new ArrayList<>();
+
+                    if (!target.isEmpty()) {
+                        FormData a = target.get(0);
+                        for (ElementData value : a.getValues()) {
+                            if(ConvertUtils.canConvertToDouble(value.getValue())){
+                                y.add(Double.valueOf(value.getValue().toString()));
+                            }
+                        }
+
+                    }
+                    //含水率是x轴
+                    if (!target1.isEmpty()) {
+                        FormData a = target1.get(0);
+                        for (ElementData value : a.getValues()) {
+                            if(ConvertUtils.canConvertToDouble(value.getValue())){
+                                x.add(Double.valueOf(value.getValue().toString()));
+                            }
+                        }
+                    }
+                    String url = "";
+                    if(CollectionUtil.isNotEmpty(x) && CollectionUtil.isNotEmpty(y)){
+                        //数据错误
+                        if(x.size() != y.size()){
+                            break;
+                        }
+                        double[][] points = new double[x.size()][2];
+                        for (int i = 0; i < x.size(); i++) {
+                            points[i][0] = x.get(i);
+                            points[i][1] = y.get(i);
+                        }
+
+                        Long id = SnowFlakeUtil.getId();
+
+                        //图片存放路径
+                        String filePath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+                        String listPdf = filePath + "/pdf/" + id + ".png";
+//                        String listPdf = "C:\\upload\\pdf\\" + id + ".jpg";
+
+                        //土界含水率试验检测记录表
+                        if(fd.getCode().contains("m_20230410104313_1645256088125177856")){
+                            // 创建图表生成器
+                            SoilTestChart generator = new SoilTestChart(points);
+                            // 生成图表并保存为图片(不显示GUI窗口)
+                            generator.generateChart(listPdf, 330, 360);
+                        }else if (fd.getCode().contains("m_20230410104301_1645256037877415936")){
+                            //土工击试验
+                            // 创建图表生成器
+                            SmoothCurveChartToImage generator = new SmoothCurveChartToImage(points, "含水率(%)", "干密度(g/cm³)", "最大干密度曲线");
+
+                            // 生成图表并保存为图片
+                            generator.generateChart(listPdf,800,185);
+                        }else if (fd.getCode().contains("m_20250506162803_1919670481296818176")){
+                            //速凝剂
+                            PenetrationResistanceChart generator = new PenetrationResistanceChart();
+
+                            // 生成图表并保存为图片
+                            generator.generateChart(points, listPdf,420, 210);
+                        }
+
+                        File tabPDF = ResourceUtil.getFile(listPdf);
+                        //上传至oss
+                        BladeFile bladeFile = this.newIOSSClient.uploadFile(id + ".jpg", listPdf);
+                        if (tabPDF.exists()) {
+                            tabPDF.delete();
+                        }
+                        if (bladeFile != null) {
+                            url = bladeFile.getLink();
+                        }
+                    }
+
+                    //生成图片返回
+                    f = f.replace(m.group(), putDataWithKey(url, tec));
+                }
+            }
+            //细集料图表公式
+            if (f.contains("aggregateChart")) {
+                //x轴坐标 固定的
+                HashMap<Integer, Double> map1 = new HashMap<>();
+                map1.put(0, 4.75);
+                map1.put(1, 2.36);
+                map1.put(2, 1.18);
+                map1.put(3, 0.6);
+                map1.put(4, 0.3);
+                map1.put(5, 0.15);
+                HashMap<Integer, Double> map = new HashMap<>();
+
+
+                String tf = f.replaceAll("^T\\(com.mixsmart.utils.CustomFunction\\)\\.\\w+\\(", "").replaceAll("[)]$", "");
+                List<String> list = Arrays.asList(tf.split(","));
+                if(list.size() == 18){
+
+                    List<List<String>> partition1 = Lists.partition(list, 3);
+                    List<FormData> target = new ArrayList<>();
+                    //重新计算索引
+                    int x = 0;
+                    for (int i = 0; i < partition1.size(); i++) {
+                        List<String> strings = partition1.get(i);
+                        //按组取获取数据
+                        List<FormData> target1 = getFormDataByCode(StringUtils.join( strings,","), tec);
+                        //全部都能获取到数据
+                        if(target1.size() == strings.size()){
+                            target.addAll(target1);
+                            map.put(x,map1.get(i));
+                            x++;
+                        }
+                    }
+
+                    if(CollectionUtil.isNotEmpty(target)){
+                        //分组
+                        List<List<FormData>> partition = Lists.partition(target, 3);
+                        //长度是否匹配
+                        String url = "";
+
+                        double[][] data = new double[partition.size()][5];
+
+                        for (int i = 0; i < partition.size(); i++) {
+                            double[] doubles = new double[5];
+                            //每组的x坐标
+                            doubles[0] = map.get(i);
+                            List<FormData> formData = partition.get(i);
+
+                            //数据是否正常
+                            boolean flag = true;
+
+                            for (int j = 0; j < formData.size(); j++) {
+                                FormData formData1 = formData.get(j);
+                                if (CollectionUtil.isEmpty(formData1.getValues())) {
+                                    flag = false;
+                                    break;
+                                }
+                                if (formData1.getValues().get(0).getValue() == null) {
+                                    flag = false;
+                                    break;
+                                }
+                                String string = formData1.getValues().get(0).getValue().toString();
+                                if (StringUtil.isBlank(string)) {
+                                    flag = false;
+                                    break;
+                                }
+                                //最后一个范围数据
+                                if (string.contains("~")) {
+                                    String[] split = string.split("~");
+                                    if (ConvertUtils.canConvertToDouble(split[0])) {
+                                        doubles[j + 1] = Double.valueOf(split[0]);
+                                    }
+                                    if (ConvertUtils.canConvertToDouble(split[1])) {
+                                        doubles[j + 2] = Double.valueOf(split[1]);
+                                    }
+                                } else {
+                                    if (ConvertUtils.canConvertToDouble(string)) {
+                                        doubles[j + 1] = Double.valueOf(string);
+                                    } else {
+                                        flag = false;
+                                    }
+                                }
+
+
+                            }
+                            //数据正常才添加坐标
+                            if (flag) {
+                                data[i] = doubles;
+                            }
+                        }
+
+                        Long id = SnowFlakeUtil.getId();
+
+                        //图片存放路径
+                        String filePath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+                        String listPdf = filePath + "/pdf/" + id + ".png";
+//                        String listPdf = "C:\\upload\\pdf\\" + id + ".jpg";
+
+
+                        // 创建图表生成器
+                        SieveAnalysisChart generator = new SieveAnalysisChart(data);
+
+                        // 生成图表并保存为图片
+                        generator.generateChart(listPdf, 800, 400);
+
+                        File tabPDF = ResourceUtil.getFile(listPdf);
+                        //上传至oss
+                        BladeFile bladeFile = this.newIOSSClient.uploadFile(id + ".jpg", listPdf);
+                        if (tabPDF.exists()) {
+                            tabPDF.delete();
+                        }
+                        if (bladeFile != null) {
+                            url = bladeFile.getLink();
+                        }
+                        //生成图片返回
+                        f = putDataWithKey(url, tec);
+                    }
+                }
+            }
             if (f.contains("converge")) {
                 Matcher m = RegexUtils.matcher(FC_REG + "(converge)\\(([^)]+)\\)", f);
                 while (m.find()) {

+ 36 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/ConvertUtils.java

@@ -0,0 +1,36 @@
+package org.springblade.manager.utils;
+
+import java.util.Optional;
+
+/**
+ * 类型转换
+ * @author LHB
+ */
+public class ConvertUtils {
+
+    public static boolean canConvertToDouble(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        // 如果是数字类型,除了Double,还有Integer、Long等都可以转成double
+        if (obj instanceof Number) {
+            return true;
+        }
+        // 如果是字符串
+        if (obj instanceof String) {
+            String str = (String) obj;
+            // 去除千分位中的逗号
+            String withoutCommas = str.replace(",", "");
+            try {
+                // 尝试转换
+                Double.parseDouble(withoutCommas);
+                return true;
+            } catch (NumberFormatException e) {
+                // 转换失败
+                return false;
+            }
+        }
+        // 其他类型,无法转换
+        return false;
+    }
+}

+ 272 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/PenetrationResistanceChart.java

@@ -0,0 +1,272 @@
+package org.springblade.manager.utils;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYLineAnnotation;
+import org.jfree.chart.annotations.XYTextAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleEdge;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.chart.ui.TextAnchor;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 凝结时间差
+ * @author LHB
+ */
+public class PenetrationResistanceChart {
+
+    /**
+     * 生成贯入阻力图表并保存为图片
+     * @param data 二维数组,第一列为时间(分钟),第二列为贯入阻力(MPa)
+     * @param outputPath 输出图片路径
+     * @return 成功返回true,失败返回false
+     */
+    public static boolean generateChart(double[][] data, String outputPath, int width, int height) {
+        try {
+            // 创建数据集 - 主曲线
+            XYSeries mainSeries = new XYSeries("贯入阻力");
+            for (double[] point : data) {
+                mainSeries.add(point[0], point[1]);
+            }
+
+            XYSeriesCollection dataset = new XYSeriesCollection();
+            dataset.addSeries(mainSeries);
+
+            // 创建支持中文的字体
+            Font chineseFont = new Font("SimSun", Font.BOLD, 12);
+            Font titleFont = new Font("SimHei", Font.BOLD, 16);
+            Font legendFont = new Font("SimSun", Font.BOLD, 10);
+
+            // 创建图表
+            JFreeChart chart = ChartFactory.createXYLineChart(
+                    "减水剂凝结时间差试验曲线",
+                    "时间 (分钟)",
+                    "贯入阻力 (MPa)",
+                    dataset,
+                    PlotOrientation.VERTICAL,
+                    true,  // 包含图例
+                    true,
+                    false
+            );
+
+            // 设置中文字体
+            chart.getTitle().setFont(titleFont);
+            chart.getLegend().setItemFont(legendFont);
+
+            // 设置图表背景和边距
+            chart.setBackgroundPaint(Color.WHITE);
+            chart.setPadding(new RectangleInsets(15, 15, 15, 15));
+
+            // 获取图表区域对象
+            XYPlot plot = (XYPlot) chart.getPlot();
+            plot.setBackgroundPaint(Color.WHITE);
+            plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
+            plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+            plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5));
+
+            // 配置X轴(设置中文字体)
+            NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
+            xAxis.setAutoRangeIncludesZero(true);
+            xAxis.setLabelFont(chineseFont);
+            xAxis.setTickLabelFont(chineseFont);
+            xAxis.setTickMarkOutsideLength(5.0f);
+
+            // 配置Y轴(设置中文字体)
+            NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
+            yAxis.setAutoRangeIncludesZero(true);
+            yAxis.setLabelFont(chineseFont);
+            yAxis.setTickLabelFont(chineseFont);
+            yAxis.setTickMarkOutsideLength(5.0f);
+
+            plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+
+            // 设置渲染器
+            XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
+
+            // 主曲线样式
+            renderer.setSeriesPaint(0, Color.BLUE);
+            renderer.setSeriesFillPaint(0, new Color(255,255,255,255));
+            //设置空心
+            renderer.setUseFillPaint(true);
+            renderer.setSeriesShape(0, new java.awt.geom.Ellipse2D.Double(-3, -3, 6, 6));
+            // 主曲线不显示点
+            renderer.setSeriesShapesVisible(0, true);
+            plot.setRenderer(0, renderer);
+
+            // 查找3.5MPa和28MPa对应的点
+            double target1 = 3.5;
+            double target2 = 28.0;
+            Double timeAt35 = null;
+            Double resistanceAt35 = target1;
+            Double timeAt28 = null;
+            Double resistanceAt28 = target2;
+
+            for (int i = 0; i < data.length - 1; i++) {
+                // 查找3.5MPa
+                if (timeAt35 == null &&
+                        ((data[i][1] <= target1 && data[i+1][1] >= target1) ||
+                                (data[i][1] >= target1 && data[i+1][1] <= target1))) {
+                    // 线性插值计算时间
+                    double x1 = data[i][0];
+                    double y1 = data[i][1];
+                    double x2 = data[i+1][0];
+                    double y2 = data[i+1][1];
+                    timeAt35 = x1 + (target1 - y1) * (x2 - x1) / (y2 - y1);
+                }
+
+                // 查找28MPa
+                if (timeAt28 == null &&
+                        ((data[i][1] <= target2 && data[i+1][1] >= target2) ||
+                                (data[i][1] >= target2 && data[i+1][1] <= target2))) {
+                    // 线性插值计算时间
+                    double x1 = data[i][0];
+                    double y1 = data[i][1];
+                    double x2 = data[i+1][0];
+                    double y2 = data[i+1][1];
+                    timeAt28 = x1 + (target2 - y1) * (x2 - x1) / (y2 - y1);
+                }
+
+                if (timeAt35 != null && timeAt28 != null) break;
+            }
+
+
+            // 为A点和B点添加虚拟系列用于图例
+            XYSeriesCollection pointDataset = new XYSeriesCollection();
+            renderer = new XYLineAndShapeRenderer();
+            int index = 0;
+            // 添加特殊点到数据集
+            if (timeAt35 != null) {
+                // 创建特殊点数据集
+                XYSeries point35Series = new XYSeries(String.format("初凝点(3.5MPa, %.0f分钟)", timeAt35));
+                point35Series.add(timeAt35, resistanceAt35);
+                pointDataset.addSeries(point35Series);
+                // 3.5MPa点样式
+                renderer.setSeriesPaint(index, Color.RED);
+                renderer.setSeriesStroke(index, new BasicStroke(1.0f));
+                // 圆形标记
+                renderer.setSeriesShape(index, new java.awt.geom.Ellipse2D.Double(-5, -5, 10, 10));
+                renderer.setSeriesShapesVisible(index, true);
+                // 不连接线
+                renderer.setSeriesLinesVisible(index, false);
+                index++;
+            }
+
+            if (timeAt28 != null) {
+                XYSeries point28Series = new XYSeries(String.format("终凝点(28MPa,%.0f分钟)", timeAt28));
+                point28Series.add(timeAt28, resistanceAt28);
+                pointDataset.addSeries(point28Series);
+                // 28MPa点样式
+                renderer.setSeriesPaint(index, Color.GREEN);
+                renderer.setSeriesStroke(index, new BasicStroke(1.0f));
+                // 圆形标记
+                renderer.setSeriesShape(index, new java.awt.geom.Ellipse2D.Double(-5, -5, 10, 10));
+                renderer.setSeriesShapesVisible(index, true);
+                // 不连接线
+                renderer.setSeriesLinesVisible(index, false);
+
+            }
+            //自定义点位
+            plot.setDataset(1, pointDataset);
+            //自定义点位样式
+            plot.setRenderer(1,renderer);
+
+
+
+
+            // 添加3.5MPa的标注和虚线
+            if (timeAt35 != null) {
+
+
+                // 水平虚线
+                XYLineAnnotation hLine35 = new XYLineAnnotation(
+                        plot.getDomainAxis().getLowerBound(), target1,
+                        plot.getDomainAxis().getUpperBound(), target1,
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.RED
+                );
+                plot.addAnnotation(hLine35);
+
+                // 垂直虚线
+                XYLineAnnotation vLine35 = new XYLineAnnotation(
+                        timeAt35, plot.getRangeAxis().getLowerBound(),
+                        timeAt35, plot.getRangeAxis().getUpperBound(),
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.RED
+                );
+                plot.addAnnotation(vLine35);
+            }
+
+            // 添加28MPa的标注和虚线
+            if (timeAt28 != null) {
+                // 水平虚线
+                XYLineAnnotation hLine28 = new XYLineAnnotation(
+                        plot.getDomainAxis().getLowerBound(), target2,
+                        plot.getDomainAxis().getUpperBound(), target2,
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.GREEN
+                );
+                plot.addAnnotation(hLine28);
+
+                // 垂直虚线
+                XYLineAnnotation vLine28 = new XYLineAnnotation(
+                        timeAt28, plot.getRangeAxis().getLowerBound(),
+                        timeAt28, plot.getRangeAxis().getUpperBound(),
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.GREEN
+                );
+                plot.addAnnotation(vLine28);
+            }
+
+            // 调整图例位置到左上角
+            chart.getLegend().setBackgroundPaint(new Color(255, 255, 255, 200)); // 半透明背景
+            chart.getLegend().setPosition(RectangleEdge.TOP);
+
+
+            // 保存图表为PNG文件
+            File outputFile = new File(outputPath);
+            ChartUtils.saveChartAsPNG(outputFile, chart, width, height);
+
+            return true;
+        } catch (IOException e) {
+            System.err.println("保存图表时发生错误: " + e.getMessage());
+            return false;
+        } catch (Exception e) {
+            System.err.println("生成图表时发生错误: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public static void main11(String[] args) {
+        // 示例数据:时间(分钟)和贯入阻力(MPa)
+        double[][] data = {
+                {0, 0.5}, {10, 1.2}, {20, 2.1}, {30, 3.0}, {40, 3.8},
+                {50, 5.2}, {60, 7.5}, {70, 10.3}, {80, 14.2}, {90, 18.5},
+                {100, 22.8}, {110, 26.5}, {120, 29.2}, {130, 31.5}, {140, 33.2},
+                {150, 34.5}, {160, 35.2}, {170, 35.8}, {180, 36.0}
+        };
+
+        // 生成图表并保存为图片
+        boolean success = generateChart(data, "penetration_resistance_chart.png",420,210);
+
+        if (success) {
+            System.out.println("图表已成功生成并保存为 'penetration_resistance_chart.png'");
+        } else {
+            System.out.println("图表生成失败");
+        }
+    }
+}

+ 217 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SieveAnalysisChart.java

@@ -0,0 +1,217 @@
+package org.springblade.manager.utils;
+
+import com.mixsmart.utils.RegexUtils;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.NumberTickUnit;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.DatasetRenderingOrder;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.LineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.category.DefaultCategoryDataset;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.regex.Matcher;
+
+/**
+ * 筛分分析图表生成工具类
+ */
+public class SieveAnalysisChart {
+    private double[][] data;
+
+    /**
+     * 构造函数
+     * @param data 筛分分析数据,格式: {{筛孔尺寸, 通过率, 累计筛余率, 第二种数据下限, 第二种数据上限}, ...}
+     */
+    public SieveAnalysisChart(double[][] data) {
+        this.data = data;
+    }
+
+    /**
+     * 生成筛分分析图表并保存为图片
+     * @param filename 输出文件名
+     * @param width 图片宽度
+     * @param height 图片高度
+     * @throws IOException 当保存文件出错时抛出
+     */
+    public void generateChart(String filename, int width, int height) throws IOException {
+        // 创建数据集 - 累计筛余率
+        DefaultCategoryDataset retainedDataset = new DefaultCategoryDataset();
+        // 创建数据集 - 通过率
+        DefaultCategoryDataset passingDataset = new DefaultCategoryDataset();
+        // 创建第二种数据 - 范围下限
+        DefaultCategoryDataset secondDataLowerDataset = new DefaultCategoryDataset();
+        // 创建第二种数据 - 范围上限
+        DefaultCategoryDataset secondDataUpperDataset = new DefaultCategoryDataset();
+
+        // 填充数据
+        for (double[] point : data) {
+            String sieveSize = String.valueOf(point[0]);
+            double retainedPercent = point[1];
+            double passingPercent = point[2];
+            double secondLower = point[3];
+            double secondUpper = point[4];
+
+            retainedDataset.addValue(retainedPercent, "累计筛余率", sieveSize);
+            passingDataset.addValue(passingPercent, "通过率", sieveSize);
+            secondDataLowerDataset.addValue(secondLower, "第二种数据下限", sieveSize);
+            secondDataUpperDataset.addValue(secondUpper, "第二种数据上限", sieveSize);
+        }
+
+        // 创建图表(不显示图例)
+        JFreeChart chart = ChartFactory.createLineChart(
+                // 标题
+                "集料级配曲线",
+                // X轴标题
+                "筛孔尺寸(mm)",
+                // 不显示左侧Y轴标题
+                "",
+                // 数据集
+                retainedDataset,
+                PlotOrientation.VERTICAL,
+                // 不显示图例
+                false,
+                false,
+                false
+        );
+
+        // 设置中文字体,防止乱码
+        Font chineseFont = new Font("SimSun", Font.BOLD, 12);
+        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 16));
+
+        // 获取图表区域对象
+        CategoryPlot plot = chart.getCategoryPlot();
+
+        // 设置左侧Y轴 (累计筛余率)
+        NumberAxis retainedAxis = (NumberAxis) plot.getRangeAxis();
+        retainedAxis.setRange(0, 100);
+        retainedAxis.setTickUnit(new NumberTickUnit(10));
+        retainedAxis.setLabelFont(chineseFont);
+        retainedAxis.setTickLabelFont(chineseFont);
+        // 在轴线内侧添加标签
+        retainedAxis.setLabel("累计筛余率(%)");
+        retainedAxis.setLabelInsets(new RectangleInsets(0, 0, 0, 0));
+
+        // 创建并设置右侧Y轴 (通过率)
+        NumberAxis passingAxis = new NumberAxis("");
+        // 正常范围设置
+        passingAxis.setRange(0, 100);
+        // 反转Y轴,使100在顶部,0在底部
+        passingAxis.setInverted(true);
+        passingAxis.setTickUnit(new NumberTickUnit(10));
+        passingAxis.setLabelFont(chineseFont);
+        passingAxis.setTickLabelFont(chineseFont);
+        // 在轴线内侧添加标签
+        passingAxis.setLabel("通过率百分比(%)");
+        passingAxis.setLabelInsets(new RectangleInsets(0, 0, 0, 0));
+
+        // 设置X轴
+        CategoryAxis domainAxis = plot.getDomainAxis();
+        domainAxis.setLabelFont(chineseFont);
+        domainAxis.setTickLabelFont(chineseFont);
+
+        // 将右侧Y轴添加到图表
+        plot.setRangeAxis(1, passingAxis);
+
+        plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+
+        // 添加通过率数据集
+        plot.setDataset(1, passingDataset);
+        // 将第二个数据集映射到第二个Y轴
+        plot.mapDatasetToRangeAxis(1, 1);
+
+        // 添加第二种数据范围数据集
+        plot.setDataset(2, secondDataLowerDataset);
+        plot.setDataset(3, secondDataUpperDataset);
+        // 映射到左侧Y轴
+        plot.mapDatasetToRangeAxis(2, 0);
+        // 映射到左侧Y轴
+        plot.mapDatasetToRangeAxis(3, 0);
+
+        // 设置渲染器 - 累计筛余率(实线,不显示数据点)
+        LineAndShapeRenderer retainedRenderer = (LineAndShapeRenderer) plot.getRenderer();
+        // 不显示数据点
+        retainedRenderer.setDefaultShapesVisible(false);
+        retainedRenderer.setSeriesPaint(0, Color.BLACK);
+        // 设置渲染器 - 通过率(实线,不显示数据点)
+        LineAndShapeRenderer passingRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        passingRenderer.setDefaultShapesVisible(false);
+        passingRenderer.setSeriesPaint(0, Color.BLACK);
+        plot.setRenderer(1, passingRenderer);
+
+        // 设置渲染器 - 第二种数据范围(虚线,不显示数据点)
+        LineAndShapeRenderer secondLowerRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        secondLowerRenderer.setDefaultShapesVisible(false);
+
+        // 设置虚线样式
+        secondLowerRenderer.setSeriesStroke(0, new BasicStroke(
+                1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
+                1.0f, new float[] {6.0f, 6.0f}, 0.0f
+        ));
+        // 设置颜色
+        secondLowerRenderer.setSeriesPaint(0, Color.BLACK);
+
+        LineAndShapeRenderer secondUpperRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        secondUpperRenderer.setDefaultShapesVisible(false);
+        // 设置虚线样式
+        secondUpperRenderer.setSeriesStroke(0, new BasicStroke(
+                1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
+                1.0f, new float[] {6.0f, 6.0f}, 0.0f
+        ));
+        // 设置颜色
+        secondUpperRenderer.setSeriesPaint(0, Color.BLACK);
+
+        // 设置渲染器
+        plot.setRenderer(2, secondLowerRenderer);
+        plot.setRenderer(3, secondUpperRenderer);
+
+        // 设置数据集渲染顺序,确保所有线都可见
+        plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
+
+        // 设置背景网格线为实线
+        plot.setDomainGridlineStroke(new BasicStroke(1.0f));
+        plot.setRangeGridlineStroke(new BasicStroke(1.0f));
+        plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
+        plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+        // 确保网格线可见
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+
+        // 设置背景颜色为白色
+        plot.setBackgroundPaint(Color.WHITE);
+
+        // 保存为图片文件
+        ChartUtils.saveChartAsPNG(new File(filename), chart, width, height);
+    }
+    public static final String FC_REG = "T\\(com.mixsmart.utils.CustomFunction\\)\\.";
+    /**
+     * 使用示例
+     */
+    public static void main11(String[] args) {
+        try {
+            // 示例数据格式: {{筛孔尺寸, 通过率, 累计筛余率, 第二种数据下限, 第二种数据上限}, ...}
+            double[][] data = {
+                    {4.75, 5.0, 85.0, 0.0, 10.0},
+                    {2.36, 25.0, 55.0, 0.0, 35.0}
+            };
+
+            // 创建图表生成器
+            SieveAnalysisChart generator = new SieveAnalysisChart(data);
+
+            // 生成图表并保存为图片
+            generator.generateChart("sieve_analysis_chart.png", 800, 400);
+            System.out.println("图表已保存为 sieve_analysis_chart.png");
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+        }
+    }
+}

+ 278 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SmoothCurveChartToImage.java

@@ -0,0 +1,278 @@
+package org.springblade.manager.utils;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYLineAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYSplineRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 最大干密度曲线生成器
+ */
+public class SmoothCurveChartToImage {
+    private double[][] dataPoints;
+    private String xAxisLabel;
+    private String yAxisLabel;
+    private String chartTitle;
+
+    /**
+     * 构造函数
+     * @param dataPoints 二维数组,每行表示一个点 [x, y]
+     * @param xAxisLabel X轴标签
+     * @param yAxisLabel Y轴标签
+     * @param chartTitle 图表标题
+     */
+    public SmoothCurveChartToImage(double[][] dataPoints, String xAxisLabel, String yAxisLabel, String chartTitle) {
+        this.dataPoints = dataPoints;
+        this.xAxisLabel = xAxisLabel;
+        this.yAxisLabel = yAxisLabel;
+        this.chartTitle = chartTitle;
+    }
+
+    /**
+     * 生成图表并保存为图片
+     * @param filePath 文件路径
+     * @param width 图片宽度
+     * @param height 图片高度
+     */
+    public void generateChart(String filePath, int width, int height) {
+        // 创建数据集
+        XYSeries series = new XYSeries("数据点");
+
+        // 添加数据点
+        for (double[] point : dataPoints) {
+            if (point.length >= 2) {
+                series.add(point[0], point[1]);
+            }
+        }
+
+        XYSeriesCollection dataset = new XYSeriesCollection(series);
+
+        // 创建图表,不显示图例
+        JFreeChart chart = ChartFactory.createXYLineChart(
+                chartTitle,
+                xAxisLabel,
+                yAxisLabel,
+                dataset,
+                PlotOrientation.VERTICAL,
+                false,
+                true,
+                false
+        );
+
+        // 设置中文字体,防止乱码
+        chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16));
+
+        // 获取图表区域
+        XYPlot plot = chart.getXYPlot();
+
+        // 设置X轴和Y轴标签字体
+        NumberAxis domainAxis = (NumberAxis) plot.getDomainAxis();
+        domainAxis.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12));
+        domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+
+        NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
+        rangeAxis.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12));
+        rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+
+        // 调整Y轴范围,不从0开始
+        double minY = series.getMinY();
+        double maxY = series.getMaxY();
+        double range = maxY - minY;
+
+        // 设置Y轴范围,从最小值下方10%开始,到最大值上方10%结束
+        double lowerBound = minY - range * 0.1;
+        double upperBound = maxY + range * 0.1;
+
+        rangeAxis.setRange(lowerBound, upperBound);
+
+        // 调整X轴范围,使其更加合适
+        double minX = series.getMinX();
+        double maxX = series.getMaxX();
+        double xRange = maxX - minX;
+        domainAxis.setRange(minX - xRange * 0.1, maxX + xRange * 0.1);
+
+        // 使用样条渲染器创建平滑曲线
+        XYSplineRenderer renderer = new XYSplineRenderer();
+        renderer.setSeriesPaint(0, Color.BLUE);
+        renderer.setSeriesStroke(0, new BasicStroke(2.0f));
+        // 显示数据点
+        renderer.setSeriesShapesVisible(0, true);
+        // 设置数据点的形状为圆形
+        renderer.setSeriesShape(0, new Ellipse2D.Double(-3, -3, 6, 6));
+
+        plot.setRenderer(renderer);
+
+        // 设置图表背景和网格线
+        plot.setBackgroundPaint(Color.LIGHT_GRAY);
+        plot.setDomainGridlinePaint(Color.WHITE);
+        plot.setRangeGridlinePaint(Color.WHITE);
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+
+        // 计算曲线上的峰值点
+        double[] peakPoint = findPeakOnCurve(dataset);
+        double peakX = peakPoint[0];
+        double peakY = peakPoint[1];
+
+        System.out.println("找到峰值点: (" + peakX + ", " + peakY + ")");
+
+        // 创建一个新的系列只包含峰值点
+        XYSeries peakSeries = new XYSeries("峰值点");
+        peakSeries.add(peakX, peakY);
+        dataset.addSeries(peakSeries);
+
+        // 为峰值点系列设置不同的渲染属性
+        renderer.setSeriesPaint(1, Color.RED);
+        renderer.setSeriesShapesVisible(1, true);
+        renderer.setSeriesShape(1, new Ellipse2D.Double(-3, -3, 6, 6));
+        renderer.setSeriesLinesVisible(1, false); // 不连接线
+
+        // 添加垂直线到X轴
+        XYLineAnnotation xLine = new XYLineAnnotation(
+                peakX, peakY, peakX, plot.getRangeAxis().getLowerBound(),
+                new BasicStroke(1.5f), Color.RED);
+        plot.addAnnotation(xLine);
+
+        // 添加垂直线到Y轴
+        XYLineAnnotation yLine = new XYLineAnnotation(
+                plot.getDomainAxis().getLowerBound(), peakY, peakX, peakY,
+                new BasicStroke(1.5f), Color.RED);
+        plot.addAnnotation(yLine);
+
+        // 设置背景网格线为实线
+        plot.setDomainGridlineStroke(new BasicStroke(1.0f));
+        plot.setRangeGridlineStroke(new BasicStroke(1.0f));
+        plot.setDomainGridlinePaint(Color.BLACK);
+        plot.setRangeGridlinePaint(Color.BLACK);
+        // 确保网格线可见
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+        plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0));
+
+        // 保存图表为图片文件
+        try {
+            File file = new File(filePath);
+            ChartUtils.saveChartAsPNG(file, chart, width, height);
+            System.out.println("图表已保存为: " + file.getAbsolutePath());
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+        }
+    }
+
+    // 在曲线上查找峰值点的方法
+    private static double[] findPeakOnCurve(XYSeriesCollection dataset) {
+        XYSeries series = dataset.getSeries(0);
+        int pointCount = series.getItemCount();
+
+        // 获取X和Y范围
+        double minX = series.getMinX();
+        double maxX = series.getMaxX();
+
+        // 在曲线上采样多个点来找到峰值
+        int samples = 1000; // 采样点数量
+        double step = (maxX - minX) / samples;
+
+        double peakX = minX;
+        double peakY = Double.NEGATIVE_INFINITY;
+
+        // 遍历采样点
+        for (int i = 0; i <= samples; i++) {
+            double x = minX + i * step;
+            double y = calculateSplineValue(series, x);
+
+            if (y > peakY) {
+                peakY = y;
+                peakX = x;
+            }
+        }
+
+        return new double[]{peakX, peakY};
+    }
+
+    // 计算样条曲线上某点的Y值
+    private static double calculateSplineValue(XYSeries series, double x) {
+        int n = series.getItemCount();
+
+        // 如果x超出数据范围,返回边界值
+        if (n == 0) return 0;
+        if (n == 1) return series.getY(0).doubleValue();
+
+        if (x <= series.getX(0).doubleValue()) {
+            return series.getY(0).doubleValue();
+        }
+        if (x >= series.getX(n - 1).doubleValue()) {
+            return series.getY(n - 1).doubleValue();
+        }
+
+        // 找到x所在的区间
+        int i = 0;
+        while (i < n - 1 && series.getX(i + 1).doubleValue() < x) {
+            i++;
+        }
+
+        // 获取相邻四个点用于三次样条计算
+        double x0 = (i > 0) ? series.getX(i - 1).doubleValue() : series.getX(i).doubleValue();
+        double x1 = series.getX(i).doubleValue();
+        double x2 = series.getX(i + 1).doubleValue();
+        double x3 = (i < n - 2) ? series.getX(i + 2).doubleValue() : series.getX(i + 1).doubleValue();
+
+        double y0 = (i > 0) ? series.getY(i - 1).doubleValue() : series.getY(i).doubleValue();
+        double y1 = series.getY(i).doubleValue();
+        double y2 = series.getY(i + 1).doubleValue();
+        double y3 = (i < n - 2) ? series.getY(i + 2).doubleValue() : series.getY(i + 1).doubleValue();
+
+        // 使用Catmull-Rom样条插值计算y值
+        return catmullRomSpline(x, x0, x1, x2, x3, y0, y1, y2, y3);
+    }
+
+    // Catmull-Rom样条插值算法
+    private static double catmullRomSpline(double x, double x0, double x1, double x2, double x3,
+                                           double y0, double y1, double y2, double y3) {
+        // 参数化t (0到1之间)
+        double t = (x - x1) / (x2 - x1);
+
+        // Catmull-Rom样条公式
+        double t2 = t * t;
+        double t3 = t2 * t;
+
+        return 0.5 * ((2 * y1) +
+                (-y0 + y2) * t +
+                (2 * y0 - 5 * y1 + 4 * y2 - y3) * t2 +
+                (-y0 + 3 * y1 - 3 * y2 + y3) * t3);
+    }
+
+    // 示例使用方法
+    public static void main11(String[] args) {
+        // 示例数据:二维数组,每行表示一个点 [含水率, 干密度]
+        double[][] points = {
+                {8.0, 1.75},
+                {12.0, 1.92},
+                {16.0, 2.05},
+                {20.0, 1.98},
+                {24.0, 1.86}
+        };
+
+        // 创建图表生成器
+        SmoothCurveChartToImage generator = new SmoothCurveChartToImage(
+                points,
+                "含水率(%)",
+                "干密度(g/cm³)",
+                "最大干密度曲线"
+        );
+
+        // 生成图表并保存为图片
+        generator.generateChart("SmoothCurveChart.png", 800, 185);
+    }
+}

+ 314 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SoilTestChart.java

@@ -0,0 +1,314 @@
+package org.springblade.manager.utils;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYTextAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.NumberTickUnit;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * 界限含水率
+ * @author LHB
+ */
+public class SoilTestChart {
+
+    // 定义关键数据点
+    private double[][] dataPoints; // 存储原始数据点
+    private double w_avg; // C'点含水率
+    private double wl;    // WL含水率
+
+    public SoilTestChart(double[][] points) {
+        // 确保数据点按含水率排序(a点含水率最大,在最右边)
+        this.dataPoints = sortDataPoints(points);
+    }
+
+    // 按含水率排序数据点(确保a点在最右边)
+    private double[][] sortDataPoints(double[][] points) {
+        // 复制数组以避免修改原始数据
+        double[][] sorted = new double[points.length][2];
+        for (int i = 0; i < points.length; i++) {
+            sorted[i][0] = points[i][0]; // 含水率
+            sorted[i][1] = points[i][1]; // 锥入深度
+        }
+
+        // 按含水率升序排序
+        Arrays.sort(sorted, Comparator.comparingDouble(a -> a[0]));
+        return sorted;
+    }
+
+    private XYDataset createDataset() {
+        // 提取排序后的点
+        double moistureC = dataPoints[0][0]; // C点含水率(最小)
+        double depthC = dataPoints[0][1];    // C点锥入深度
+
+        double moistureB = dataPoints[1][0]; // B点含水率
+        double depthB = dataPoints[1][1];    // B点锥入深度
+
+        double moistureA = dataPoints[2][0]; // A点含水率(最大)
+        double depthA = dataPoints[2][1];    // A点锥入深度
+
+        // 计算AB和AC线上h=2mm时的含水率
+        double w_ab = calculateWaterContent(moistureA, depthA, moistureB, depthB, 2.0);
+        double w_ac = calculateWaterContent(moistureA, depthA, moistureC, depthC, 2.0);
+
+        // 计算平均含水率作为C'点
+        w_avg = (w_ab + w_ac) / 2.0;
+
+        // 创建AC'线数据集 - 只包含A点和C'点
+        XYSeries seriesACPrime = new XYSeries("AC'线");
+        seriesACPrime.add(moistureA, depthA); // A点
+        seriesACPrime.add(w_avg, 2.0);        // C'点
+
+        // 计算h=20mm时的含水率(WL)
+        wl = calculateWaterContent(moistureA, depthA, w_avg, 2.0, 20.0);
+
+        // 创建WL和WP的虚线数据集
+        XYSeries seriesWL = new XYSeries("WL");
+        seriesWL.add(wl, 0);
+        seriesWL.add(wl, 20);
+
+        XYSeries seriesWP = new XYSeries("WP");
+        seriesWP.add(w_avg, 0);
+        seriesWP.add(w_avg, 2);
+
+        XYSeries seriesWLHorizontal = new XYSeries("WL_H");
+        seriesWLHorizontal.add(0, 20);
+        seriesWLHorizontal.add(wl, 20);
+
+        XYSeries seriesWPHorizontal = new XYSeries("WP_H");
+        seriesWPHorizontal.add(0, 2);
+        seriesWPHorizontal.add(w_avg, 2);
+
+        // 创建原始点数据集
+        XYSeries seriesPoints = new XYSeries("原始点");
+        seriesPoints.add(moistureA, depthA); // A点
+        seriesPoints.add(moistureB, depthB); // B点
+        seriesPoints.add(moistureC, depthC); // C点
+
+        // 创建C'点数据集(单独显示)
+        XYSeries seriesCPrime = new XYSeries("C'点");
+        seriesCPrime.add(w_avg, 2.0);
+
+        // 将所有系列添加到数据集
+        XYSeriesCollection dataset = new XYSeriesCollection();
+        dataset.addSeries(seriesACPrime);
+        dataset.addSeries(seriesWL);
+        dataset.addSeries(seriesWP);
+        dataset.addSeries(seriesWLHorizontal);
+        dataset.addSeries(seriesWPHorizontal);
+        dataset.addSeries(seriesPoints);
+        dataset.addSeries(seriesCPrime);
+
+        return dataset;
+    }
+
+    // 计算直线上给定深度对应的含水率
+    private double calculateWaterContent(double x1, double y1, double x2, double y2, double targetY) {
+        double k = (y2 - y1) / (x2 - x1);
+        double b = y1 - k * x1;
+        return (targetY - b) / k;
+    }
+
+    private JFreeChart createChart(XYDataset dataset) {
+        // 提取排序后的点
+        double moistureC = dataPoints[0][0]; // C点含水率(最小)
+        double depthC = dataPoints[0][1];    // C点锥入深度
+
+        double moistureB = dataPoints[1][0]; // B点含水率
+        double depthB = dataPoints[1][1];    // B点锥入深度
+
+        double moistureA = dataPoints[2][0]; // A点含水率(最大)
+        double depthA = dataPoints[2][1];    // A点锥入深度
+
+        // 创建折线图
+        JFreeChart chart = ChartFactory.createXYLineChart(
+                "锥入深度与含水率(h-w)关系",      // 图表标题
+                "含水率w(%)",           // x轴标签
+                "锥入深度h(mm)",       // y轴标签
+                dataset,              // 数据集
+                PlotOrientation.VERTICAL,
+                false,               // 不显示图例
+                false,               // 不显示提示
+                false                // 不显示URL
+        );
+
+        // 调整图表边距
+        chart.setPadding(new RectangleInsets(10, 10, 10, 10));
+
+        // 解决中文乱码问题
+        chart.getTitle().setFont(new Font("宋体", Font.BOLD, 18));
+
+        // 获取图表区域对象
+        XYPlot plot = chart.getXYPlot();
+
+        // 设置x轴
+        NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
+        xAxis.setLabelFont(new Font("宋体", Font.BOLD, 14));
+        xAxis.setTickLabelFont(new Font("宋体", Font.BOLD, 12));
+        xAxis.setTickMarkOutsideLength(-5.0f);
+        xAxis.setTickMarksVisible(true);
+        xAxis.setAutoRangeIncludesZero(false);
+        // 设置x轴刻度间隔为10
+        xAxis.setTickUnit(new NumberTickUnit(10));
+        // 设置x轴范围,根据数据调整
+        xAxis.setRange(0, 100);
+
+        // 设置y轴
+        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
+        yAxis.setLabelFont(new Font("宋体", Font.BOLD, 14));
+        yAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
+        yAxis.setTickMarkOutsideLength(-5.0f);
+        yAxis.setTickMarksVisible(true);
+
+        //设置坐标轴偏移
+        plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+        // 设置渲染器
+        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
+
+        // AC'线 - 实线(只连接A点和C'点)
+        renderer.setSeriesPaint(0, Color.BLACK);
+        renderer.setSeriesStroke(0, new BasicStroke(2.0f));
+        renderer.setSeriesShapesVisible(0, false); // 不显示形状
+
+        // WL虚线 - 红色
+        renderer.setSeriesPaint(1, Color.BLACK);
+        renderer.setSeriesStroke(1, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(1, false);
+
+        // WP虚线 - 绿色
+        renderer.setSeriesPaint(2, Color.BLACK);
+        renderer.setSeriesStroke(2, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(2, false);
+
+        // WL水平虚线 - 红色
+        renderer.setSeriesPaint(3, Color.BLACK);
+        renderer.setSeriesStroke(3, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(3, false);
+
+        // WP水平虚线 - 绿色
+        renderer.setSeriesPaint(4, Color.BLACK);
+        renderer.setSeriesStroke(4, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(4, false);
+
+        // 原始点 - 不连线,只显示形状
+        renderer.setSeriesPaint(5, Color.BLACK);
+        renderer.setSeriesLinesVisible(5, false);
+        renderer.setSeriesShapesVisible(5, true);
+        renderer.setSeriesShape(5, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
+
+        // C'点 - 不连线,只显示形状
+        renderer.setSeriesPaint(6, Color.BLACK);
+        renderer.setSeriesLinesVisible(6, false);
+        renderer.setSeriesShapesVisible(6, true);
+        renderer.setSeriesShape(6, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
+
+        plot.setRenderer(renderer);
+
+        // 设置背景色
+        plot.setBackgroundPaint(Color.WHITE);
+        // 移除水平方向网格线
+        plot.setDomainGridlinesVisible(false);
+        // 移除垂直方向网格线
+        plot.setRangeGridlinesVisible(false);
+        // 禁用绘图区域外边框
+//        plot.setOutlineVisible(false);
+
+        // A点标签(含水率最大,在最右边)
+        XYTextAnnotation aLabel = new XYTextAnnotation("A", moistureA - 1, depthA + 0.5);
+        aLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        aLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(aLabel);
+
+        // B点标签
+        XYTextAnnotation bLabel = new XYTextAnnotation("B", moistureB - 1, depthB + 0.5);
+        bLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        bLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(bLabel);
+
+        // C点标签(含水率最小,在最左边)
+        XYTextAnnotation cLabel = new XYTextAnnotation("C", moistureC - 1, depthC + 0.5);
+        cLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        cLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(cLabel);
+
+        // C'点标签
+        XYTextAnnotation cPrimeLabel = new XYTextAnnotation("C'", w_avg - 1, 2.0 + 0.5);
+        cPrimeLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        cPrimeLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(cPrimeLabel);
+
+        // WL标签(x轴上方)
+        XYTextAnnotation wlLabel = new XYTextAnnotation("WL", wl - 1, 0.5);
+        wlLabel.setFont(new Font("宋体", Font.BOLD, 12));
+        wlLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(wlLabel);
+
+        // WP标签(x轴上方)
+        XYTextAnnotation wpLabel = new XYTextAnnotation("WP", w_avg - 1, 0.5);
+        wpLabel.setFont(new Font("宋体", Font.BOLD, 12));
+        wpLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(wpLabel);
+
+        return chart;
+    }
+
+    // 生成图表并保存为图片
+    public void generateChart(String filename, int width, int height) {
+        try {
+            // 创建数据集
+            XYDataset dataset = createDataset();
+
+            // 创建图表
+            JFreeChart chart = createChart(dataset);
+
+            // 保存图表为图片
+            ChartUtils.saveChartAsPNG(new File(filename), chart, width, height);
+            System.out.println("图表已保存为 " + filename);
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    public static void main11(String[] args) {
+        // 示例数据:二维数组,每行表示一个点 [含水率, 锥入深度]
+        // 注意:a点应该含水率最大,在最右边
+        double[][] points = {
+                {25.0, 5.0},  // C点(含水率最小)
+                {35.0, 15.0}, // B点
+                {45.0, 25.0}  // A点(含水率最大)
+        };
+
+        // 创建图表生成器
+        SoilTestChart generator = new SoilTestChart(points);
+
+        // 生成图表并保存为图片(不显示GUI窗口)
+        generator.generateChart("SoilTestChart.png", 500, 600);
+    }
+}