张世豪
6 天以前 b315a6943e6c0d6bdf0d5f7565c570d719154d6c
src/dikuai/Dikuaiguanli.java
@@ -6,6 +6,9 @@
import java.awt.datatransfer.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import ui.UIConfig;
@@ -17,8 +20,11 @@
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import lujing.Lunjingguihua;
import lujing.MowingPathGenerationPage;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
@@ -58,6 +64,8 @@
   private JButton addLandBtn;
   private static String currentWorkLandNumber;
   private static final String WORK_LAND_KEY = "currentWorkLandNumber";
   private static final String PROPERTIES_FILE = "set.properties";
   private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>();
   private ImageIcon workSelectedIcon;
   private ImageIcon workUnselectedIcon;
@@ -218,12 +226,12 @@
      
      // 地块编号
      contentPanel.add(createCardInfoItem("地块编号:", getDisplayValue(dikuai.getLandNumber(), "未知")));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      // 添加时间
      contentPanel.add(createCardInfoItem("添加时间:", getDisplayValue(dikuai.getCreateTime(), "未知")));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      // 地块面积
      String landArea = dikuai.getLandArea();
      if (landArea != null && !landArea.equals("-1")) {
@@ -232,14 +240,14 @@
         landArea = "未知";
      }
      contentPanel.add(createCardInfoItem("地块面积:", landArea));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      // 返回点坐标(带修改按钮)
      contentPanel.add(createCardInfoItemWithButton("返回点坐标:",
         getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"),
      contentPanel.add(createCardInfoItemWithButton("返回点坐标:",
         getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"),
         "修改", e -> editReturnPoint(dikuai)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      // 地块边界坐标(带显示顶点按钮)
      JPanel boundaryPanel = createBoundaryInfoItem(dikuai,
         getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "未设置"));
@@ -248,27 +256,31 @@
         () -> editBoundaryCoordinates(dikuai),
         "点击查看/编辑地块边界坐标");
      contentPanel.add(boundaryPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber());
      JPanel obstaclePanel = createCardInfoItemWithButton("障碍物:",
         obstacleSummary.buildDisplayValue(),
         "新增",
         e -> addNewObstacle(dikuai));
      setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip());
      // 让障碍物标题可点击,打开障碍物管理页面
      configureInteractiveLabel(getInfoItemTitleLabel(obstaclePanel),
         () -> showObstacleManagementPage(dikuai),
         "点击查看/管理障碍物");
      contentPanel.add(obstaclePanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      // 路径坐标(带查看按钮)
      JPanel pathPanel = createCardInfoItemWithButton("路径坐标:",
         getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"),
      JPanel pathPanel = createCardInfoItemWithButton("路径坐标:",
         getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("路径坐标", dikuai.getPlannedPath()));
      setInfoItemTooltip(pathPanel, dikuai.getPlannedPath());
      configureInteractiveLabel(getInfoItemTitleLabel(pathPanel),
         () -> editPlannedPath(dikuai),
         "点击查看/编辑路径坐标");
      contentPanel.add(pathPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      JPanel baseStationPanel = createCardInfoItemWithButton("基站坐标:",
         getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "未设置"),
@@ -278,7 +290,7 @@
         () -> editBaseStationCoordinates(dikuai),
         "点击查看/编辑基站坐标");
      contentPanel.add(baseStationPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      JPanel boundaryOriginalPanel = createCardInfoItemWithButton("边界原始坐标:",
         getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "未设置"),
@@ -288,7 +300,7 @@
         () -> editBoundaryOriginalCoordinates(dikuai),
         "点击查看/编辑边界原始坐标");
      contentPanel.add(boundaryOriginalPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      JPanel mowingPatternPanel = createCardInfoItemWithButton("割草模式:",
         getTruncatedValue(dikuai.getMowingPattern(), 12, "未设置"),
@@ -298,7 +310,7 @@
         () -> editMowingPattern(dikuai),
         "点击查看/编辑割草模式");
      contentPanel.add(mowingPatternPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      String mowingWidthValue = dikuai.getMowingWidth();
      String widthSource = null;
@@ -311,7 +323,7 @@
         "编辑", e -> editMowingWidth(dikuai));
      setInfoItemTooltip(mowingWidthPanel, widthSource);
      contentPanel.add(mowingWidthPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
      JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:",
         getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"),
@@ -327,8 +339,8 @@
      JButton deleteBtn = createDeleteButton();
      deleteBtn.addActionListener(e -> deleteDikuai(dikuai));
      JButton generatePathBtn = createPrimaryFooterButton("生成割草路径");
      generatePathBtn.addActionListener(e -> generateMowingPath(dikuai));
      JButton generatePathBtn = createPrimaryFooterButton("路径规划");
      generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai));
      JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      footerPanel.setBackground(CARD_BACKGROUND);
@@ -363,7 +375,10 @@
   private JPanel createCardInfoItemWithButton(String label, String value, String buttonText, ActionListener listener) {
      JPanel itemPanel = new JPanel(new BorderLayout());
      itemPanel.setBackground(CARD_BACKGROUND);
      itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));
      // 增加高度以确保按钮完整显示(按钮高度约24-28像素,加上上下边距)
      itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35));
      itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30));
      itemPanel.setMinimumSize(new Dimension(0, 28));
      
      JLabel labelComp = new JLabel(label);
      labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
@@ -371,13 +386,14 @@
      
      JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
      rightPanel.setBackground(CARD_BACKGROUND);
      // 添加垂直内边距以确保按钮不被裁剪
      rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
      
      JLabel valueComp = new JLabel(value);
      valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
      valueComp.setForeground(TEXT_COLOR);
      
      JButton button = createSmallButton(buttonText);
      button.addActionListener(listener);
      JButton button = createSmallLinkButton(buttonText, listener);
      
      rightPanel.add(valueComp);
      rightPanel.add(button);
@@ -393,17 +409,19 @@
      private JPanel createBoundaryInfoItem(Dikuai dikuai, String displayValue) {
         JPanel itemPanel = new JPanel(new BorderLayout());
         itemPanel.setBackground(CARD_BACKGROUND);
         int rowHeight = Math.max(36, BOUNDARY_TOGGLE_ICON_SIZE + 12);
         // 增加高度以确保按钮下边缘完整显示(按钮高度56,加上上下边距)
         int rowHeight = Math.max(60, BOUNDARY_TOGGLE_ICON_SIZE + 16);
         Dimension rowDimension = new Dimension(Integer.MAX_VALUE, rowHeight);
         itemPanel.setMaximumSize(rowDimension);
         itemPanel.setPreferredSize(rowDimension);
         itemPanel.setMinimumSize(new Dimension(0, 32));
         itemPanel.setMinimumSize(new Dimension(0, 56));
         JLabel labelComp = new JLabel("地块边界:");
         labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
         labelComp.setForeground(LIGHT_TEXT);
         int verticalPadding = Math.max(0, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2);
         // 确保按钮有足够的上下边距,避免下边缘被裁剪
         int verticalPadding = Math.max(2, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2);
         JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
         rightPanel.setBackground(CARD_BACKGROUND);
         rightPanel.setBorder(BorderFactory.createEmptyBorder(verticalPadding, 0, verticalPadding, 0));
@@ -504,6 +522,7 @@
               boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber);
               if (isCurrent) {
                  renderer.setBoundaryPointsVisible(desiredState);
                  renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d);
               }
            }
         }
@@ -583,17 +602,69 @@
      JScrollPane scrollPane = new JScrollPane(textArea);
      scrollPane.setPreferredSize(new Dimension(360, 240));
      int option = JOptionPane.showConfirmDialog(
         this,
         scrollPane,
         title,
         JOptionPane.OK_CANCEL_OPTION,
         JOptionPane.PLAIN_MESSAGE);
      if (option == JOptionPane.OK_OPTION) {
         return textArea.getText();
      Window owner = SwingUtilities.getWindowAncestor(this);
      JDialog dialog;
      if (owner instanceof Frame) {
         dialog = new JDialog((Frame) owner, title, true);
      } else if (owner instanceof Dialog) {
         dialog = new JDialog((Dialog) owner, title, true);
      } else {
         dialog = new JDialog((Frame) null, title, true);
      }
      return null;
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      JPanel contentPanel = new JPanel(new BorderLayout());
      contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
      contentPanel.add(scrollPane, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      JButton okButton = new JButton("确定");
      JButton cancelButton = new JButton("取消");
      JButton copyButton = new JButton("复制");
      final boolean[] confirmed = new boolean[] {false};
      final String[] resultHolder = new String[1];
      okButton.addActionListener(e -> {
         resultHolder[0] = textArea.getText();
         confirmed[0] = true;
         dialog.dispose();
      });
      cancelButton.addActionListener(e -> dialog.dispose());
      copyButton.addActionListener(e -> {
         String text = textArea.getText();
         if (text == null) {
            text = "";
         }
         String trimmed = text.trim();
         if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            JOptionPane.showMessageDialog(dialog, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
         }
         try {
            StringSelection selection = new StringSelection(text);
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(selection, selection);
            JOptionPane.showMessageDialog(dialog, title + " 已复制到剪贴板", "提示", JOptionPane.INFORMATION_MESSAGE);
         } catch (Exception ex) {
            JOptionPane.showMessageDialog(dialog, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
         }
      });
      buttonPanel.add(okButton);
      buttonPanel.add(cancelButton);
      buttonPanel.add(copyButton);
      contentPanel.add(buttonPanel, BorderLayout.SOUTH);
      dialog.setContentPane(contentPanel);
      dialog.getRootPane().setDefaultButton(okButton);
      dialog.pack();
      dialog.setLocationRelativeTo(this);
      dialog.setVisible(true);
      return confirmed[0] ? resultHolder[0] : null;
   }
   private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) {
@@ -733,10 +804,26 @@
      switch (trimmed) {
         case "1":
         case "spiral":
         case "螺旋":
         case "螺旋模式":
            return "spiral";
         case "0":
         case "parallel":
         case "平行":
         case "平行模式":
         default:
            if (trimmed.contains("螺旋")) {
               return "spiral";
            }
            if (trimmed.contains("spiral")) {
               return "spiral";
            }
            if (trimmed.contains("parallel")) {
               return "parallel";
            }
            if (trimmed.contains("平行")) {
               return "parallel";
            }
            return "parallel";
      }
   }
@@ -752,6 +839,88 @@
      return trimmed;
   }
   /**
    * 显示路径规划页面
    */
   private void showPathPlanningPage(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      Window owner = SwingUtilities.getWindowAncestor(this);
      // 获取地块管理对话框,准备在打开路径规划页面时关闭
      Window managementWindow = null;
      if (owner instanceof JDialog) {
         managementWindow = owner;
      }
      // 获取地块基本数据
      String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates());
      String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates());
      List<Obstacledge.Obstacle> configuredObstacles = getConfiguredObstacles(dikuai);
      String obstacleValue = determineInitialObstacleValue(dikuai, configuredObstacles);
      String widthValue = sanitizeWidthString(dikuai.getMowingWidth());
      if (widthValue != null) {
         try {
            double widthCm = Double.parseDouble(widthValue);
            widthValue = formatWidthForStorage(widthCm);
         } catch (NumberFormatException ignored) {
            // 保持原始字符串,稍后校验提示
         }
      }
      String modeValue = sanitizeValueOrNull(dikuai.getMowingPattern());
      String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
      // 创建保存回调接口实现
      MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() {
         @Override
         public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value);
         }
         @Override
         public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value);
         }
         @Override
         public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) {
            return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue);
         }
         @Override
         public boolean saveMowingWidth(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "mowingWidth", value);
         }
         @Override
         public boolean savePlannedPath(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "plannedPath", value);
         }
      };
      // 显示路径规划页面
      MowingPathGenerationPage dialog = new MowingPathGenerationPage(
         owner,
         dikuai,
         baseStationValue,
         boundaryValue,
         obstacleValue,
         widthValue,
         modeValue,
         existingPath,
         callback
      );
      // 关闭地块管理页面
      if (managementWindow != null) {
         managementWindow.dispose();
      }
      dialog.setVisible(true);
   }
   private void generateMowingPath(Dikuai dikuai) {
      if (dikuai == null) {
         return;
@@ -790,178 +959,48 @@
      String modeValue,
      String initialGeneratedPath) {
      Window owner = SwingUtilities.getWindowAncestor(this);
      JDialog dialog = new JDialog(owner, "生成割草路径", Dialog.ModalityType.APPLICATION_MODAL);
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      dialog.getContentPane().setLayout(new BorderLayout());
      dialog.getContentPane().setBackground(BACKGROUND_COLOR);
      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));
      JTextField baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true);
      contentPanel.add(createTextFieldSection("基站坐标", baseStationField));
      JTextArea boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6);
      contentPanel.add(createTextAreaSection("地块边界", boundaryArea));
      JTextArea obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6);
      contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea));
      JTextField widthField = createInfoTextField(widthValue != null ? widthValue : "", true);
      contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField));
      contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue)));
      String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
      String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath;
      JTextArea pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10);
      contentPanel.add(createTextAreaSection("割草路径坐标", pathArea));
      JScrollPane dialogScrollPane = new JScrollPane(contentPanel);
      dialogScrollPane.setBorder(BorderFactory.createEmptyBorder());
      dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16);
      dialog.add(dialogScrollPane, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12));
      buttonPanel.setBackground(BACKGROUND_COLOR);
      JButton generateBtn = createPrimaryFooterButton("生成割草路径");
      JButton saveBtn = createPrimaryFooterButton("保存路径");
      JButton cancelBtn = createPrimaryFooterButton("取消");
      generateBtn.addActionListener(e -> {
         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);
            }
      // 创建保存回调接口实现
      MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() {
         @Override
         public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value);
         }
         String generated = attemptMowingPathPreview(
            boundaryArea.getText(),
            obstacleArea.getText(),
            sanitizedWidth,
            modeValue,
            dialog,
            true
         );
         if (generated != null) {
            pathArea.setText(generated);
            pathArea.setCaretPosition(0);
         @Override
         public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value);
         }
      });
      saveBtn.addActionListener(e -> {
         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";
            }
         @Override
         public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) {
            return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue);
         }
         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";
            }
         @Override
         public boolean saveMowingWidth(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "mowingWidth", value);
         }
         String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
         String widthSanitized = sanitizeWidthString(widthField.getText());
         if (widthSanitized == null) {
            String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
            JOptionPane.showMessageDialog(dialog, message, "提示", JOptionPane.WARNING_MESSAGE);
            return;
         @Override
         public boolean savePlannedPath(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "plannedPath", value);
         }
         double widthCm;
         try {
            widthCm = Double.parseDouble(widthSanitized);
         } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(dialog, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE);
            return;
         }
         if (widthCm <= 0) {
            JOptionPane.showMessageDialog(dialog, "割草宽度必须大于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(dialog, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", baseStationNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存基站坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", boundaryNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存地块边界", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!persistObstaclesForLand(dikuai, baseStationNormalized, obstacleNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "mowingWidth", widthNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存割草宽度", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "plannedPath", pathNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         JOptionPane.showMessageDialog(dialog, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
         dialog.dispose();
      });
      cancelBtn.addActionListener(e -> dialog.dispose());
      buttonPanel.add(generateBtn);
      buttonPanel.add(saveBtn);
      buttonPanel.add(cancelBtn);
      dialog.add(buttonPanel, BorderLayout.SOUTH);
      dialog.pack();
      dialog.setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
      dialog.setLocationRelativeTo(owner);
      };
      // 使用新的独立页面类
      MowingPathGenerationPage dialog = new MowingPathGenerationPage(
         owner,
         dikuai,
         baseStationValue,
         boundaryValue,
         obstacleValue,
         widthValue,
         modeValue,
         initialGeneratedPath,
         callback
      );
      dialog.setVisible(true);
   }
@@ -1274,112 +1313,104 @@
      return value;
   }
   /**
    * 创建类似于链接的小按钮
    */
   private JButton createSmallLinkButton(String text, ActionListener listener) {
      JButton btn = new JButton(text);
      btn.setFont(new Font("微软雅黑", Font.PLAIN, 11));
      btn.setForeground(PRIMARY_COLOR);
      btn.setBorder(BorderFactory.createCompoundBorder(
         BorderFactory.createLineBorder(PRIMARY_COLOR, 1, true),
         BorderFactory.createEmptyBorder(2, 6, 2, 6)
      ));
      btn.setContentAreaFilled(false);
      btn.setFocusPainted(false);
      btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
      btn.addMouseListener(new MouseAdapter() {
         public void mouseEntered(MouseEvent e) { btn.setOpaque(true); btn.setBackground(new Color(230, 250, 240)); }
         public void mouseExited(MouseEvent e) { btn.setOpaque(false); }
      });
      if (listener != null) {
         btn.addActionListener(listener);
      }
      return btn;
   }
   private JButton createSmallButton(String text) {
      return createSmallButton(text, PRIMARY_COLOR, PRIMARY_DARK);
      return createSmallLinkButton(text, null);
   }
   private JButton createSmallButton(String text, Color backgroundColor, Color hoverColor) {
      JButton button = new JButton(text);
      button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
      // 对于需要不同颜色的按钮,使用实心风格
      Color baseColor = backgroundColor == null ? PRIMARY_COLOR : backgroundColor;
      Color hover = hoverColor == null ? baseColor : hoverColor;
      button.setBackground(baseColor);
      button.setForeground(WHITE);
      button.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
      button.setMargin(new Insets(0, 0, 0, 0));
      button.setFocusPainted(false);
      button.setCursor(new Cursor(Cursor.HAND_CURSOR));
      button.addMouseListener(new MouseAdapter() {
         public void mouseEntered(MouseEvent e) {
            button.setBackground(hover);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(baseColor);
         }
      });
      return button;
      return createStyledButton(text, baseColor, true);
   }
   private JButton createActionButton(String text, Color color) {
      JButton button = new JButton(text);
      button.setFont(new Font("微软雅黑", Font.PLAIN, 14));
      button.setBackground(color);
      button.setForeground(WHITE);
      button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16));
      button.setFocusPainted(false);
      button.setCursor(new Cursor(Cursor.HAND_CURSOR));
      return createStyledButton(text, color, true); // 实心风格
   }
      // 悬停效果
      button.addMouseListener(new MouseAdapter() {
         public void mouseEntered(MouseEvent e) {
            if (color == RED_COLOR) {
               button.setBackground(RED_DARK);
   /**
    * 创建现代风格按钮 (实心/轮廓)
    */
   private JButton createStyledButton(String text, Color baseColor, boolean filled) {
      JButton btn = new JButton(text) {
         @Override
         protected void paintComponent(Graphics g) {
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            boolean isPressed = getModel().isPressed();
            boolean isRollover = getModel().isRollover();
            if (filled) {
               if (isPressed) g2.setColor(baseColor.darker());
               else if (isRollover) g2.setColor(baseColor.brighter());
               else g2.setColor(baseColor);
               g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8);
               g2.setColor(Color.WHITE);
            } else {
               button.setBackground(PRIMARY_DARK);
               g2.setColor(CARD_BACKGROUND); // 背景
               g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8);
               if (isPressed) g2.setColor(baseColor.darker());
               else if (isRollover) g2.setColor(baseColor);
               else g2.setColor(new Color(200, 200, 200)); // 默认边框灰
               g2.setStroke(new BasicStroke(1.2f));
               g2.drawRoundRect(0, 0, getWidth()-1, getHeight()-1, 8, 8);
               g2.setColor(isRollover ? baseColor : TEXT_COLOR);
            }
            FontMetrics fm = g2.getFontMetrics();
            int x = (getWidth() - fm.stringWidth(getText())) / 2;
            int y = (getHeight() - fm.getHeight()) / 2 + fm.getAscent();
            g2.drawString(getText(), x, y);
            g2.dispose();
         }
         public void mouseExited(MouseEvent e) {
            if (color == RED_COLOR) {
               button.setBackground(RED_COLOR);
            } else {
               button.setBackground(PRIMARY_COLOR);
            }
         }
      });
      return button;
      };
      btn.setFocusPainted(false);
      btn.setContentAreaFilled(false);
      btn.setBorderPainted(false);
      btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
      btn.setFont(new Font("微软雅黑", Font.BOLD, 12));
      return btn;
   }
   private JButton createDeleteButton() {
      JButton button = new JButton("删除");
      button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
      button.setBackground(RED_COLOR);
      button.setForeground(WHITE);
      button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12));
      button.setFocusPainted(false);
      button.setCursor(new Cursor(Cursor.HAND_CURSOR));
      JButton button = createStyledButton("删除", RED_COLOR, false); // 轮廓风格
      ImageIcon deleteIcon = loadIcon("image/delete.png", 16, 16);
      if (deleteIcon != null) {
         button.setIcon(deleteIcon);
         button.setIconTextGap(6);
      }
      // 悬停效果
      button.addMouseListener(new MouseAdapter() {
         public void mouseEntered(MouseEvent e) {
            button.setBackground(RED_DARK);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(RED_COLOR);
         }
      });
      return button;
   }
   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 MouseAdapter() {
         public void mouseEntered(MouseEvent e) {
            button.setBackground(PRIMARY_DARK);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(PRIMARY_COLOR);
         }
      });
      return button;
      return createStyledButton(text, PRIMARY_COLOR, true); // 实心风格
   }
   private JButton createWorkToggleButton(Dikuai dikuai) {
@@ -1446,10 +1477,19 @@
   }
   public static void setCurrentWorkLand(String landNumber, String landName) {
      currentWorkLandNumber = landNumber;
      String sanitizedLandNumber = sanitizeLandNumber(landNumber);
      boolean changed = !Objects.equals(currentWorkLandNumber, sanitizedLandNumber);
      if (!changed) {
         String persisted = readPersistedWorkLandNumber();
         if (!Objects.equals(persisted, sanitizedLandNumber)) {
            changed = true;
         }
      }
      currentWorkLandNumber = sanitizedLandNumber;
      Dikuai dikuai = null;
      if (landNumber != null) {
         dikuai = Dikuai.getDikuai(landNumber);
      if (sanitizedLandNumber != null) {
         dikuai = Dikuai.getDikuai(sanitizedLandNumber);
         if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) {
            landName = dikuai.getLandName();
         }
@@ -1460,7 +1500,7 @@
      Shouye shouye = Shouye.getInstance();
      if (shouye != null) {
         if (landNumber == null) {
         if (sanitizedLandNumber == null) {
            shouye.updateCurrentAreaName(null);
         } else {
            shouye.updateCurrentAreaName(landName);
@@ -1470,24 +1510,87 @@
            renderer.applyLandMetadata(dikuai);
            String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
            String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
            List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : Collections.emptyList();
            List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList();
            if (configuredObstacles == null) {
               configuredObstacles = Collections.emptyList();
            }
            renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
            renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName);
            renderer.setCurrentPlannedPath(plannedPath);
            renderer.setCurrentObstacles(configuredObstacles, landNumber);
            boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false);
            renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber);
            boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false);
            renderer.setBoundaryPointsVisible(showBoundaryPoints);
            renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d);
            // 退出预览后,不显示障碍物点(障碍物点只在预览时显示)
            renderer.setObstaclePointsVisible(false);
         }
         shouye.refreshMowingIndicators();
      }
      if (changed) {
         persistCurrentWorkLand(sanitizedLandNumber);
      }
   }
   public static String getCurrentWorkLandNumber() {
      return currentWorkLandNumber;
   }
   public static String getPersistedWorkLandNumber() {
      return readPersistedWorkLandNumber();
   }
   private static String sanitizeLandNumber(String landNumber) {
      if (landNumber == null) {
         return null;
      }
      String trimmed = landNumber.trim();
      if (trimmed.isEmpty() || "-1".equals(trimmed)) {
         return null;
      }
      return trimmed;
   }
   private static void persistCurrentWorkLand(String landNumber) {
      synchronized (Dikuaiguanli.class) {
         Properties props = new Properties();
         try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) {
            props.load(in);
         } catch (IOException ignored) {
            // Use empty defaults when the configuration file is missing.
         }
         if (landNumber == null) {
            props.setProperty(WORK_LAND_KEY, "-1");
         } else {
            props.setProperty(WORK_LAND_KEY, landNumber);
         }
         try (FileOutputStream out = new FileOutputStream(PROPERTIES_FILE)) {
            props.store(out, "Current work land selection updated");
         } catch (IOException ex) {
            System.err.println("无法保存当前作业地块: " + ex.getMessage());
         }
      }
   }
   private static String readPersistedWorkLandNumber() {
      Properties props = new Properties();
      try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) {
         props.load(in);
         String value = props.getProperty(WORK_LAND_KEY);
         if (value == null) {
            return null;
         }
         String trimmed = value.trim();
         if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
         }
         return trimmed;
      } catch (IOException ex) {
         return null;
      }
   }
   private ImageIcon loadIcon(String path, int width, int height) {
      try {
         ImageIcon rawIcon = new ImageIcon(path);
@@ -1526,6 +1629,31 @@
      }
   }
   /**
    * 显示障碍物管理页面
    */
   private void showObstacleManagementPage(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      Window owner = SwingUtilities.getWindowAncestor(this);
      // 获取地块管理对话框,准备在打开障碍物管理页面时关闭
      Window managementWindow = null;
      if (owner instanceof JDialog) {
         managementWindow = owner;
      }
      ObstacleManagementPage managementPage = new ObstacleManagementPage(owner, dikuai);
      // 关闭地块管理页面
      if (managementWindow != null) {
         managementWindow.dispose();
      }
      managementPage.setVisible(true);
   }
   private void addNewObstacle(Dikuai dikuai) {
      if (dikuai == null) {
         JOptionPane.showMessageDialog(this, "未找到当前地块,无法新增障碍物", "提示", JOptionPane.WARNING_MESSAGE);