package lujing; import javax.swing.*; import javax.swing.SwingUtilities; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import dikuai.Dikuai; import lujing.Lunjingguihua; import lujing.ObstaclePathPlanner; import lujing.Qufenxingzhuang; import lujing.AoxinglujingNoObstacle; import lujing.YixinglujingNoObstacle; import lujing.AoxinglujingHaveObstacel; import lujing.YixinglujingHaveObstacel; import org.locationtech.jts.geom.Coordinate; import gecaoji.Device; import java.util.Locale; import zhuye.Fuzhibutton; /** * 生成割草路径页面 * 独立的对话框类,用于生成和编辑割草路径 */ public class MowingPathGenerationPage extends JDialog { private static final long serialVersionUID = 1L; // 尺寸常量 private static final int SCREEN_WIDTH = 400; private static final int SCREEN_HEIGHT = 800; // 颜色常量 private static final Color PRIMARY_COLOR = new Color(46, 139, 87); private static final Color PRIMARY_DARK = new Color(30, 107, 69); private static final Color TEXT_COLOR = new Color(51, 51, 51); private static final Color WHITE = Color.WHITE; private static final Color BORDER_COLOR = new Color(200, 200, 200); private static final Color BACKGROUND_COLOR = new Color(250, 250, 250); // 数据保存回调接口 public interface PathSaveCallback { boolean saveBaseStationCoordinates(Dikuai dikuai, String value); boolean saveBoundaryCoordinates(Dikuai dikuai, String value); boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue); boolean saveMowingWidth(Dikuai dikuai, String value); boolean savePlannedPath(Dikuai dikuai, String value); } private final Dikuai dikuai; private final PathSaveCallback saveCallback; // UI组件 private JTextField baseStationField; private JTextArea boundaryArea; private JTextArea obstacleArea; private JTextField widthField; private JTextArea pathArea; /** * 构造函数 * @param owner 父窗口 * @param dikuai 地块对象 * @param baseStationValue 基站坐标 * @param boundaryValue 地块边界 * @param obstacleValue 障碍物坐标 * @param widthValue 割草宽度 * @param modeValue 割草模式 * @param initialGeneratedPath 初始生成的路径 * @param saveCallback 保存回调接口 */ public MowingPathGenerationPage(Window owner, Dikuai dikuai, String baseStationValue, String boundaryValue, String obstacleValue, String widthValue, String modeValue, String initialGeneratedPath, PathSaveCallback saveCallback) { super(owner, "路径规划页面", Dialog.ModalityType.APPLICATION_MODAL); this.dikuai = dikuai; this.saveCallback = saveCallback; setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); getContentPane().setLayout(new BorderLayout()); getContentPane().setBackground(BACKGROUND_COLOR); initializeUI(baseStationValue, boundaryValue, obstacleValue, widthValue, modeValue, initialGeneratedPath); pack(); setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT)); setLocationRelativeTo(owner); } /** * 初始化UI */ private void initializeUI(String baseStationValue, String boundaryValue, String obstacleValue, String widthValue, String modeValue, String initialGeneratedPath) { JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); contentPanel.setBackground(BACKGROUND_COLOR); contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 16, 12, 16)); // 标题 String landName = getDisplayValue(dikuai.getLandName(), "未知地块"); String landNumber = getDisplayValue(dikuai.getLandNumber(), "未知编号"); JLabel headerLabel = new JLabel(landName + " / " + landNumber); headerLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); headerLabel.setForeground(TEXT_COLOR); headerLabel.setAlignmentX(Component.LEFT_ALIGNMENT); contentPanel.add(headerLabel); contentPanel.add(Box.createVerticalStrut(12)); // 基站坐标 baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true); contentPanel.add(createTextFieldSection("基站坐标", baseStationField)); // 地块边界 boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6); contentPanel.add(createTextAreaSection("地块边界", boundaryArea)); // 障碍物坐标 obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6); contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea)); // 割草宽度 widthField = createInfoTextField(widthValue != null ? widthValue : "", true); contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField)); // 割草安全距离(只读显示) String displaySafetyDistance = "未设置"; Device device = Device.getActiveDevice(); if (device != null) { String safetyDistanceValue = device.getMowingSafetyDistance(); if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) { try { double distanceMeters = Double.parseDouble(safetyDistanceValue.trim()); // 如果值大于100,认为是厘米,需要转换为米 if (distanceMeters > 100) { distanceMeters = distanceMeters / 100.0; } displaySafetyDistance = String.format("%.2f米", distanceMeters); } catch (NumberFormatException e) { displaySafetyDistance = "未设置"; } } } contentPanel.add(createInfoValueSection("割草安全距离", displaySafetyDistance)); // 割草模式(只读显示) contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue))); // 割草路径坐标 String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath()); String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath; pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10); contentPanel.add(createTextAreaSection("割草路径坐标", pathArea)); JScrollPane dialogScrollPane = new JScrollPane(contentPanel); dialogScrollPane.setBorder(BorderFactory.createEmptyBorder()); dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16); add(dialogScrollPane, BorderLayout.CENTER); // 按钮面板 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12)); buttonPanel.setBackground(BACKGROUND_COLOR); JButton generateBtn = createPrimaryFooterButton("生成割草路径"); JButton previewBtn = createPrimaryFooterButton("预览"); JButton saveBtn = createPrimaryFooterButton("保存路径"); JButton cancelBtn = createPrimaryFooterButton("取消"); generateBtn.addActionListener(e -> generatePath(modeValue)); previewBtn.addActionListener(e -> previewPath()); saveBtn.addActionListener(e -> savePath()); cancelBtn.addActionListener(e -> dispose()); buttonPanel.add(generateBtn); buttonPanel.add(previewBtn); buttonPanel.add(saveBtn); buttonPanel.add(cancelBtn); add(buttonPanel, BorderLayout.SOUTH); } /** * 生成路径 */ private void generatePath(String modeValue) { String sanitizedWidth = sanitizeWidthString(widthField.getText()); if (sanitizedWidth != null) { try { double widthCm = Double.parseDouble(sanitizedWidth); widthField.setText(formatWidthForStorage(widthCm)); sanitizedWidth = formatWidthForStorage(widthCm); } catch (NumberFormatException ex) { widthField.setText(sanitizedWidth); } } String generated = attemptMowingPathPreview( boundaryArea.getText(), obstacleArea.getText(), sanitizedWidth, modeValue, this, true ); if (generated != null) { pathArea.setText(generated); pathArea.setCaretPosition(0); } } /** * 预览路径 */ private void previewPath() { // 先保存当前路径到地块(临时保存,用于预览) String pathNormalized = normalizeCoordinateInput(pathArea.getText()); if (!"-1".equals(pathNormalized)) { pathNormalized = pathNormalized .replace("\r\n", ";") .replace('\r', ';') .replace('\n', ';') .replaceAll(";+", ";") .replaceAll("\\s*;\\s*", ";") .trim(); if (pathNormalized.isEmpty()) { pathNormalized = "-1"; } } if ("-1".equals(pathNormalized)) { JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE); return; } // 临时保存路径到地块对象(不持久化) if (saveCallback != null) { saveCallback.savePlannedPath(dikuai, pathNormalized); } // 保存当前页面状态,用于返回时恢复 String currentBaseStation = baseStationField.getText(); String currentBoundary = boundaryArea.getText(); String currentObstacle = obstacleArea.getText(); String currentWidth = widthField.getText(); String currentPath = pathArea.getText(); // 获取地块信息 String landNumber = dikuai.getLandNumber(); String landName = dikuai.getLandName(); // 处理边界坐标,确保变量是 effectively final String boundaryInput = normalizeCoordinateInput(boundaryArea.getText()); final String boundary; if (!"-1".equals(boundaryInput)) { String processed = boundaryInput.replace("\r\n", ";") .replace('\r', ';') .replace('\n', ';') .replaceAll(";+", ";") .replaceAll("\\s*;\\s*", ";") .trim(); if (processed.isEmpty()) { boundary = dikuai.getBoundaryCoordinates(); } else { boundary = processed; } } else { boundary = dikuai.getBoundaryCoordinates(); } // 处理障碍物坐标,确保变量是 effectively final String obstaclesInput = normalizeCoordinateInput(obstacleArea.getText()); final String obstacles; if (!"-1".equals(obstaclesInput)) { String processed = obstaclesInput.replace("\r\n", " ") .replace('\r', ' ') .replace('\n', ' ') .replaceAll("\\s{2,}", " ") .trim(); if (processed.isEmpty()) { obstacles = null; } else { obstacles = processed; } } else { obstacles = null; } // 保存最终值到 final 变量,以便在 lambda 中使用 final String finalPathNormalized = pathNormalized; final String finalLandNumber = landNumber; final String finalLandName = landName; // 关闭路径规划页面 setVisible(false); // 打开主页面并显示路径预览 SwingUtilities.invokeLater(() -> { zhuye.Shouye shouye = zhuye.Shouye.getInstance(); if (shouye != null) { // 显示路径预览,并设置返回回调 shouye.startMowingPathPreview( finalLandNumber, finalLandName, boundary, obstacles, finalPathNormalized, () -> { // 返回回调:重新打开路径规划页面 SwingUtilities.invokeLater(() -> { setVisible(true); // 恢复之前的状态 baseStationField.setText(currentBaseStation); boundaryArea.setText(currentBoundary); obstacleArea.setText(currentObstacle); widthField.setText(currentWidth); pathArea.setText(currentPath); }); } ); } else { // 如果主页面不存在,提示用户并重新显示路径规划页面 JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE); setVisible(true); } }); } /** * 保存路径 */ private void savePath() { String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText()); String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText()); if (!"-1".equals(boundaryNormalized)) { boundaryNormalized = boundaryNormalized .replace("\r\n", ";") .replace('\r', ';') .replace('\n', ';') .replaceAll(";+", ";") .replaceAll("\\s*;\\s*", ";") .trim(); if (boundaryNormalized.isEmpty()) { boundaryNormalized = "-1"; } } String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText()); if (!"-1".equals(obstacleNormalized)) { obstacleNormalized = obstacleNormalized .replace("\r\n", " ") .replace('\r', ' ') .replace('\n', ' ') .replaceAll("\\s{2,}", " ") .trim(); if (obstacleNormalized.isEmpty()) { obstacleNormalized = "-1"; } } String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : ""; String widthSanitized = sanitizeWidthString(widthField.getText()); if (widthSanitized == null) { String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确"; JOptionPane.showMessageDialog(this, message, "提示", JOptionPane.WARNING_MESSAGE); return; } double widthCm; try { widthCm = Double.parseDouble(widthSanitized); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE); return; } if (widthCm <= 0) { JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE); return; } String widthNormalized = formatWidthForStorage(widthCm); widthField.setText(widthNormalized); String pathNormalized = normalizeCoordinateInput(pathArea.getText()); if (!"-1".equals(pathNormalized)) { pathNormalized = pathNormalized .replace("\r\n", ";") .replace('\r', ';') .replace('\n', ';') .replaceAll(";+", ";") .replaceAll("\\s*;\\s*", ";") .trim(); if (pathNormalized.isEmpty()) { pathNormalized = "-1"; } } if ("-1".equals(pathNormalized)) { JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE); return; } // 调用回调保存数据 if (saveCallback != null) { if (!saveCallback.saveBaseStationCoordinates(dikuai, baseStationNormalized)) { JOptionPane.showMessageDialog(this, "无法保存基站坐标", "错误", JOptionPane.ERROR_MESSAGE); return; } if (!saveCallback.saveBoundaryCoordinates(dikuai, boundaryNormalized)) { JOptionPane.showMessageDialog(this, "无法保存地块边界", "错误", JOptionPane.ERROR_MESSAGE); return; } if (!saveCallback.saveObstacleCoordinates(dikuai, baseStationNormalized, obstacleNormalized)) { JOptionPane.showMessageDialog(this, "无法保存障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE); return; } if (!saveCallback.saveMowingWidth(dikuai, widthNormalized)) { JOptionPane.showMessageDialog(this, "无法保存割草宽度", "错误", JOptionPane.ERROR_MESSAGE); return; } if (!saveCallback.savePlannedPath(dikuai, pathNormalized)) { JOptionPane.showMessageDialog(this, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE); return; } } JOptionPane.showMessageDialog(this, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE); dispose(); } /** * 尝试生成路径预览 */ private String attemptMowingPathPreview(String boundaryInput, String obstacleInput, String widthCmInput, String modeInput, Component parentComponent, boolean showMessages) { String boundary = sanitizeValueOrNull(boundaryInput); if (boundary == null) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "当前地块未设置边界坐标,无法生成路径", "提示", JOptionPane.WARNING_MESSAGE); } return null; } String rawWidth = widthCmInput != null ? widthCmInput.trim() : ""; String widthStr = sanitizeWidthString(widthCmInput); if (widthStr == null) { if (showMessages) { String message = rawWidth.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确"; JOptionPane.showMessageDialog(parentComponent, message, "提示", JOptionPane.WARNING_MESSAGE); } return null; } double widthCm; try { widthCm = Double.parseDouble(widthStr); } catch (NumberFormatException ex) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE); } return null; } if (widthCm <= 0) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE); } return null; } double widthMeters = widthCm / 100.0d; String plannerWidth = BigDecimal.valueOf(widthMeters) .setScale(3, RoundingMode.HALF_UP) .stripTrailingZeros() .toPlainString(); // 检查原始输入是否有障碍物(在sanitize之前检查,避免丢失信息) String rawObstacleInput = obstacleInput != null ? obstacleInput.trim() : ""; boolean hasObstacleInput = !rawObstacleInput.isEmpty() && !"-1".equals(rawObstacleInput); String obstacles = sanitizeValueOrNull(obstacleInput); if (obstacles != null) { obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' '); } // 获取安全距离 String safetyMarginStr = getSafetyDistanceString(); if (safetyMarginStr == null) { // 如果没有设置安全距离,使用默认值:割草宽度的一半 + 0.2米 double defaultSafetyDistance = widthMeters / 2.0 + 0.2; safetyMarginStr = BigDecimal.valueOf(defaultSafetyDistance) .setScale(3, RoundingMode.HALF_UP) .stripTrailingZeros() .toPlainString(); } String mode = normalizeExistingMowingPattern(modeInput); try { // 1. 首先判断地块类型(凸形还是异形) Qufenxingzhuang shapeJudger = new Qufenxingzhuang(); int grassType = shapeJudger.judgeGrassType(boundary); // grassType: 0=无法判断, 1=凸形, 2=异形 // 解析障碍物列表 List> obstacleList = Lunjingguihua.parseObstacles(obstacles); if (obstacleList == null) { obstacleList = new ArrayList<>(); } // 判断是否有有效的障碍物:只有当解析成功且列表不为空时,才认为有障碍物 boolean hasValidObstacles = !obstacleList.isEmpty(); String generated = null; // 2. 根据地块类型和是否有障碍物,调用不同的路径生成类 if (!hasValidObstacles) { // 无障碍物的情况 if (grassType == 1) { // 凸形地块,无障碍物 -> 调用 AoxinglujingNoObstacle List segments = AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); generated = formatAoxingPathSegments(segments); } else if (grassType == 2) { // 异形地块,无障碍物 -> 调用 YixinglujingNoObstacle // 注意:如果该类还没有实现,这里会抛出异常或返回null try { // 假设 YixinglujingNoObstacle 有类似的方法签名 // 如果类还没有实现,可能需要使用原来的方法作为后备 generated = YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); } catch (Exception e) { // 如果类还没有实现,使用原来的方法作为后备 if (showMessages) { System.err.println("YixinglujingNoObstacle 尚未实现,使用默认方法: " + e.getMessage()); } generated = Lunjingguihua.generatePathFromStrings( boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode); } } else { // 无法判断地块类型,使用原来的方法作为后备 if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法", "提示", JOptionPane.WARNING_MESSAGE); } generated = Lunjingguihua.generatePathFromStrings( boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode); } } else { // 有障碍物的情况 if (grassType == 1) { // 凸形地块,有障碍物 -> 调用 AoxinglujingHaveObstacel try { // 假设 AoxinglujingHaveObstacel 有类似的方法签名 generated = AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); } catch (Exception e) { // 如果类还没有实现,使用原来的方法作为后备 if (showMessages) { System.err.println("AoxinglujingHaveObstacel 尚未实现,使用默认方法: " + e.getMessage()); } List polygon = Lunjingguihua.parseCoordinates(boundary); if (polygon.size() < 4) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", "错误", JOptionPane.ERROR_MESSAGE); } return null; } double safetyDistance = Double.parseDouble(safetyMarginStr); ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( polygon, widthMeters, mode, obstacleList, safetyDistance); List segments = pathPlanner.generate(); generated = Lunjingguihua.formatPathSegments(segments); } } else if (grassType == 2) { // 异形地块,有障碍物 -> 调用 YixinglujingHaveObstacel try { // 假设 YixinglujingHaveObstacel 有类似的方法签名 generated = YixinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); } catch (Exception e) { // 如果类还没有实现,使用原来的方法作为后备 if (showMessages) { System.err.println("YixinglujingHaveObstacel 尚未实现,使用默认方法: " + e.getMessage()); } List polygon = Lunjingguihua.parseCoordinates(boundary); if (polygon.size() < 4) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", "错误", JOptionPane.ERROR_MESSAGE); } return null; } double safetyDistance = Double.parseDouble(safetyMarginStr); ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( polygon, widthMeters, mode, obstacleList, safetyDistance); List segments = pathPlanner.generate(); generated = Lunjingguihua.formatPathSegments(segments); } } else { // 无法判断地块类型,使用原来的方法作为后备 if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法", "提示", JOptionPane.WARNING_MESSAGE); } List polygon = Lunjingguihua.parseCoordinates(boundary); if (polygon.size() < 4) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", "错误", JOptionPane.ERROR_MESSAGE); } return null; } double safetyDistance = Double.parseDouble(safetyMarginStr); ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( polygon, widthMeters, mode, obstacleList, safetyDistance); List segments = pathPlanner.generate(); generated = Lunjingguihua.formatPathSegments(segments); } } String trimmed = generated != null ? generated.trim() : ""; if (trimmed.isEmpty()) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "未生成有效的割草路径,请检查地块数据", "提示", JOptionPane.INFORMATION_MESSAGE); } return null; } if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "割草路径已生成", "成功", JOptionPane.INFORMATION_MESSAGE); } return trimmed; } catch (IllegalArgumentException ex) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "生成割草路径失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { if (showMessages) { JOptionPane.showMessageDialog(parentComponent, "生成割草路径时发生异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } ex.printStackTrace(); } return null; } /** * 获取安全距离字符串(米) */ private String getSafetyDistanceString() { Device device = Device.getActiveDevice(); if (device != null) { String safetyDistanceValue = device.getMowingSafetyDistance(); if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) { try { double distanceMeters = Double.parseDouble(safetyDistanceValue.trim()); // 如果值大于100,认为是厘米,需要转换为米 if (distanceMeters > 100) { distanceMeters = distanceMeters / 100.0; } return BigDecimal.valueOf(distanceMeters) .setScale(3, RoundingMode.HALF_UP) .stripTrailingZeros() .toPlainString(); } catch (NumberFormatException e) { // 解析失败,返回null,使用默认值 } } } return null; } /** * 格式化 AoxinglujingNoObstacle.PathSegment 列表为坐标字符串 */ private String formatAoxingPathSegments(List segments) { if (segments == null || segments.isEmpty()) { return ""; } StringBuilder sb = new StringBuilder(); AoxinglujingNoObstacle.Point last = null; for (AoxinglujingNoObstacle.PathSegment segment : segments) { // 只添加割草工作段,跳过过渡段 if (segment.isMowing) { // 如果起点与上一个终点不同,添加起点 if (last == null || !equals2D(last, segment.start)) { appendPoint(sb, segment.start); } // 添加终点 appendPoint(sb, segment.end); last = segment.end; } } return sb.toString(); } /** * 比较两个点是否相同(使用小的容差) */ private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.Point p2) { if (p1 == null || p2 == null) { return p1 == p2; } double tolerance = 1e-6; return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; } /** * 添加点到字符串构建器 */ private void appendPoint(StringBuilder sb, AoxinglujingNoObstacle.Point point) { if (sb.length() > 0) { sb.append(";"); } sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y)); } // ========== UI辅助方法 ========== private JTextArea createInfoTextArea(String text, boolean editable, int rows) { JTextArea area = new JTextArea(text); area.setEditable(editable); area.setLineWrap(true); area.setWrapStyleWord(true); area.setFont(new Font("微软雅黑", Font.PLAIN, 13)); area.setRows(Math.max(rows, 2)); area.setCaretPosition(0); area.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); area.setBackground(editable ? WHITE : new Color(245, 245, 245)); return area; } private JPanel createTextAreaSection(String title, JTextArea textArea) { JPanel section = new JPanel(new BorderLayout(0, 6)); section.setBackground(BACKGROUND_COLOR); section.setAlignmentX(Component.LEFT_ALIGNMENT); // 创建标题面板,包含标题和复制图标 JPanel titlePanel = new JPanel(new BorderLayout()); titlePanel.setBackground(BACKGROUND_COLOR); titlePanel.setOpaque(false); JLabel titleLabel = new JLabel(title); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); titleLabel.setForeground(TEXT_COLOR); titlePanel.add(titleLabel, BorderLayout.WEST); // 创建复制按钮(使用 Fuzhibutton) JButton copyButton = Fuzhibutton.createCopyButton( () -> { String text = textArea.getText(); if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) { return null; // 返回null会触发"未设置"提示 } return text; }, "复制" + title, new Color(230, 250, 240) ); titlePanel.add(copyButton, BorderLayout.EAST); section.add(titlePanel, BorderLayout.NORTH); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); scrollPane.getVerticalScrollBar().setUnitIncrement(12); section.add(scrollPane, BorderLayout.CENTER); section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); return section; } private JTextField createInfoTextField(String text, boolean editable) { JTextField field = new JTextField(text); field.setEditable(editable); field.setFont(new Font("微软雅黑", Font.PLAIN, 13)); field.setForeground(TEXT_COLOR); field.setBackground(editable ? WHITE : new Color(245, 245, 245)); field.setCaretPosition(0); field.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); field.setFocusable(true); field.setOpaque(true); return field; } private JPanel createTextFieldSection(String title, JTextField textField) { JPanel section = new JPanel(new BorderLayout(0, 6)); section.setBackground(BACKGROUND_COLOR); section.setAlignmentX(Component.LEFT_ALIGNMENT); // 创建标题面板,包含标题和复制图标 JPanel titlePanel = new JPanel(new BorderLayout()); titlePanel.setBackground(BACKGROUND_COLOR); titlePanel.setOpaque(false); JLabel titleLabel = new JLabel(title); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); titleLabel.setForeground(TEXT_COLOR); titlePanel.add(titleLabel, BorderLayout.WEST); // 创建复制按钮(使用 Fuzhibutton) JButton copyButton = Fuzhibutton.createCopyButton( () -> { String text = textField.getText(); if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) { return null; // 返回null会触发"未设置"提示 } return text; }, "复制" + title, new Color(230, 250, 240) ); titlePanel.add(copyButton, BorderLayout.EAST); section.add(titlePanel, BorderLayout.NORTH); JPanel fieldWrapper = new JPanel(new BorderLayout()); fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245)); fieldWrapper.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); fieldWrapper.add(textField, BorderLayout.CENTER); section.add(fieldWrapper, BorderLayout.CENTER); section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); return section; } private JPanel createInfoValueSection(String title, String value) { JPanel section = new JPanel(); section.setLayout(new BoxLayout(section, BoxLayout.X_AXIS)); section.setBackground(BACKGROUND_COLOR); section.setAlignmentX(Component.LEFT_ALIGNMENT); section.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); JLabel titleLabel = new JLabel(title + ":"); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); titleLabel.setForeground(TEXT_COLOR); section.add(titleLabel); section.add(Box.createHorizontalStrut(8)); JLabel valueLabel = new JLabel(value); valueLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); valueLabel.setForeground(TEXT_COLOR); section.add(valueLabel); section.add(Box.createHorizontalGlue()); return section; } private JButton createPrimaryFooterButton(String text) { JButton button = new JButton(text); button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); button.setBackground(PRIMARY_COLOR); button.setForeground(WHITE); button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12)); button.setFocusPainted(false); button.setCursor(new Cursor(Cursor.HAND_CURSOR)); button.addMouseListener(new java.awt.event.MouseAdapter() { public void mouseEntered(java.awt.event.MouseEvent e) { button.setBackground(PRIMARY_DARK); } public void mouseExited(java.awt.event.MouseEvent e) { button.setBackground(PRIMARY_COLOR); } }); return button; } // ========== 数据处理辅助方法 ========== private String getDisplayValue(String value, String defaultValue) { if (value == null || value.equals("-1") || value.trim().isEmpty()) { return defaultValue; } return value; } private String prepareCoordinateForEditor(String value) { if (value == null) { return ""; } String trimmed = value.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { return ""; } return trimmed; } private String normalizeCoordinateInput(String input) { if (input == null) { return "-1"; } String trimmed = input.trim(); return trimmed.isEmpty() ? "-1" : trimmed; } private String sanitizeValueOrNull(String input) { if (input == null) { return null; } String trimmed = input.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { return null; } return trimmed; } private String sanitizeWidthString(String input) { if (input == null) { return null; } String trimmed = input.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { return null; } String cleaned = trimmed.replaceAll("[^0-9.+-]", ""); return cleaned.isEmpty() ? null : cleaned; } private String formatWidthForStorage(double widthCm) { return BigDecimal.valueOf(widthCm) .setScale(2, RoundingMode.HALF_UP) .stripTrailingZeros() .toPlainString(); } private String formatMowingPatternForDialog(String patternValue) { String sanitized = sanitizeValueOrNull(patternValue); if (sanitized == null) { return "未设置"; } String normalized = normalizeExistingMowingPattern(sanitized); if ("parallel".equals(normalized)) { return "平行模式 (parallel)"; } if ("spiral".equals(normalized)) { return "螺旋模式 (spiral)"; } return sanitized; } private String normalizeExistingMowingPattern(String patternValue) { if (patternValue == null) { return "parallel"; } String trimmed = patternValue.trim().toLowerCase(); if ("1".equals(trimmed) || "spiral".equals(trimmed)) { return "spiral"; } return "parallel"; } }