826220679@qq.com
2 天以前 48ee74129bb09a817a0bbbabe860c4007b74c66b
src/dikuai/Dikuaiguanli.java
@@ -6,21 +6,32 @@
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 ui.UIConfig;
import ui.UIUtils;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
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 publicway.Fuzhibutton;
import publicway.Lookbutton;
import publicway.buttonset;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhuye.Coordinate;
import gecaoji.Device;
/**
 * 地块管理面板 - 卡片式布局设计
@@ -55,12 +66,15 @@
   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;
   private ImageIcon boundaryVisibleIcon;
   private ImageIcon boundaryHiddenIcon;
   private static final int BOUNDARY_TOGGLE_ICON_SIZE = 48;
   private static final int BOUNDARY_TOGGLE_ICON_SIZE = 24;
   private Map<String, ObstacleSummary> obstacleSummaryCache = Collections.emptyMap();
   public Dikuaiguanli(String landNumber) {
      latestInstance = this;
@@ -136,13 +150,15 @@
      cardsPanel.removeAll();
      
      Map<String, Dikuai> allDikuai = Dikuai.getAllDikuai();
      if (allDikuai.isEmpty()) {
         obstacleSummaryCache = Collections.emptyMap();
         // 显示空状态
         JPanel emptyPanel = createEmptyStatePanel();
         cardsPanel.add(emptyPanel);
         setCurrentWorkLand(null, null);
      } else {
         obstacleSummaryCache = loadObstacleSummaries();
         if (allDikuai.size() == 1) {
            Dikuai onlyDikuai = allDikuai.values().iterator().next();
            setCurrentWorklandIfNeeded(onlyDikuai);
@@ -151,7 +167,7 @@
         for (Dikuai dikuai : allDikuai.values()) {
            JPanel card = createDikuaiCard(dikuai);
            cardsPanel.add(card);
            cardsPanel.add(Box.createRigidArea(new Dimension(0, 15)));
            cardsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
         }
      }
      
@@ -199,8 +215,27 @@
      headerPanel.add(nameLabel, BorderLayout.WEST);
      // 右侧区域:状态文字 + 按钮
      JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
      rightPanel.setBackground(CARD_BACKGROUND);
      rightPanel.setOpaque(false);
      // 状态文字标签(根据是否选中显示/隐藏)
      JLabel statusLabel = new JLabel("已设置为当前地块");
      statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
      statusLabel.setForeground(PRIMARY_COLOR);
      boolean isCurrent = dikuai.getLandNumber() != null && dikuai.getLandNumber().equals(currentWorkLandNumber);
      statusLabel.setVisible(isCurrent);
      JButton workToggleBtn = createWorkToggleButton(dikuai);
      headerPanel.add(workToggleBtn, BorderLayout.EAST);
      // 将状态标签和按钮关联,以便在按钮状态变化时更新标签
      workToggleBtn.putClientProperty("statusLabel", statusLabel);
      rightPanel.add(statusLabel);
      rightPanel.add(workToggleBtn);
      headerPanel.add(rightPanel, BorderLayout.EAST);
      
      card.add(headerPanel, BorderLayout.NORTH);
      
@@ -212,12 +247,12 @@
      
      // 地块编号
      contentPanel.add(createCardInfoItem("地块编号:", getDisplayValue(dikuai.getLandNumber(), "未知")));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 添加时间
      contentPanel.add(createCardInfoItem("添加时间:", getDisplayValue(dikuai.getCreateTime(), "未知")));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 地块面积
      String landArea = dikuai.getLandArea();
      if (landArea != null && !landArea.equals("-1")) {
@@ -226,79 +261,142 @@
         landArea = "未知";
      }
      contentPanel.add(createCardInfoItem("地块面积:", landArea));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      // 返回点坐标(带修改按钮)
      contentPanel.add(createCardInfoItemWithButton("返回点坐标:",
         getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"),
         "修改", e -> editReturnPoint(dikuai)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      // 地块边界坐标(带显示顶点按钮)
      JPanel boundaryPanel = createBoundaryInfoItem(dikuai,
         getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "未设置"));
      setInfoItemTooltip(boundaryPanel, dikuai.getBoundaryCoordinates());
      contentPanel.add(boundaryPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      // 障碍物坐标(带新增和查看按钮)
      JPanel obstaclePanel = createCardInfoItemWithButton("障碍物坐标:",
         getTruncatedValue(dikuai.getObstacleCoordinates(), 12, "未设置"),
         "新增",
         e -> addNewObstacle(dikuai));
      setInfoItemTooltip(obstaclePanel, dikuai.getObstacleCoordinates());
      contentPanel.add(obstaclePanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 路径坐标(带查看按钮)
      JPanel pathPanel = createCardInfoItemWithButton("路径坐标:",
         getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("路径坐标", dikuai.getPlannedPath()));
      setInfoItemTooltip(pathPanel, dikuai.getPlannedPath());
      contentPanel.add(pathPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      JPanel baseStationPanel = createCardInfoItemWithButton("基站坐标:",
         getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("基站坐标", dikuai.getBaseStationCoordinates()));
      setInfoItemTooltip(baseStationPanel, dikuai.getBaseStationCoordinates());
      contentPanel.add(baseStationPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      JPanel boundaryOriginalPanel = createCardInfoItemWithButton("边界原始坐标:",
         getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("边界原始坐标", dikuai.getBoundaryOriginalCoordinates()));
      setInfoItemTooltip(boundaryOriginalPanel, dikuai.getBoundaryOriginalCoordinates());
      contentPanel.add(boundaryOriginalPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      JPanel mowingPatternPanel = createCardInfoItemWithButton("割草模式:",
         getTruncatedValue(dikuai.getMowingPattern(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("割草模式", dikuai.getMowingPattern()));
      setInfoItemTooltip(mowingPatternPanel, dikuai.getMowingPattern());
      JPanel mowingPatternPanel = createCardInfoItem("割草模式:",
         formatMowingPatternForDisplay(dikuai.getMowingPattern()));
      configureInteractiveLabel(getInfoItemTitleLabel(mowingPatternPanel),
         () -> editMowingPattern(dikuai),
         "点击查看/编辑割草模式");
      contentPanel.add(mowingPatternPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 割草机割刀宽度
      String mowingBladeWidthValue = dikuai.getMowingBladeWidth();
      String displayBladeWidth = "未设置";
      if (mowingBladeWidthValue != null && !"-1".equals(mowingBladeWidthValue) && !mowingBladeWidthValue.trim().isEmpty()) {
         try {
            double bladeWidthMeters = Double.parseDouble(mowingBladeWidthValue.trim());
            double bladeWidthCm = bladeWidthMeters * 100.0;
            displayBladeWidth = String.format("%.2f厘米", bladeWidthCm);
         } catch (NumberFormatException e) {
            displayBladeWidth = "未设置";
         }
      }
      JPanel mowingBladeWidthPanel = createCardInfoItem("割草机割刀宽度:", displayBladeWidth);
      contentPanel.add(mowingBladeWidthPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      String mowingWidthValue = dikuai.getMowingWidth();
      String widthSource = null;
      String displayWidth = "未设置";
      if (mowingWidthValue != null && !"-1".equals(mowingWidthValue) && !mowingWidthValue.trim().isEmpty()) {
         widthSource = mowingWidthValue + "厘米";
         displayWidth = mowingWidthValue + "厘米";
      }
      String displayWidth = getTruncatedValue(widthSource, 12, "未设置");
      JPanel mowingWidthPanel = createCardInfoItemWithButton("割草宽度:",
         displayWidth,
         "编辑", e -> editMowingWidth(dikuai));
      setInfoItemTooltip(mowingWidthPanel, widthSource);
      JPanel mowingWidthPanel = createCardInfoItem("割草宽度:", displayWidth);
      contentPanel.add(mowingWidthPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 割草安全距离
      String displaySafetyDistance = "未设置";
      String safetyDistanceValue = dikuai.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 = "未设置";
         }
      }
      JPanel mowingSafetyDistancePanel = createCardInfoItem("割草安全距离:", displaySafetyDistance);
      contentPanel.add(mowingSafetyDistancePanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 往返点路径(带查看图标按钮)
      JPanel returnPathPanel = createCardInfoItemWithIconButton("往返点路径:",
         createViewButton(e -> editReturnPath(dikuai)));
      configureInteractiveLabel(getInfoItemTitleLabel(returnPathPanel),
         () -> editReturnPath(dikuai),
         "点击查看/编辑往返点路径");
      contentPanel.add(returnPathPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      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, 10)));
      // 地块边界坐标(带显示顶点按钮)
      JPanel boundaryPanel = createBoundaryInfoItem(dikuai);
      configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel),
         () -> editBoundaryCoordinates(dikuai),
         "点击查看/编辑地块边界坐标");
      contentPanel.add(boundaryPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      // 路径坐标(带查看按钮)
      JPanel pathPanel = createCardInfoItemWithIconButton("路径坐标:",
         createViewButton(e -> editPlannedPath(dikuai)));
      configureInteractiveLabel(getInfoItemTitleLabel(pathPanel),
         () -> editPlannedPath(dikuai),
         "点击查看/编辑路径坐标");
      contentPanel.add(pathPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
   JPanel baseStationPanel = createCardInfoItemWithIconButton("基站坐标:",
      createViewButton(e -> editBaseStationCoordinates(dikuai)));
   configureInteractiveLabel(getInfoItemTitleLabel(baseStationPanel),
      () -> editBaseStationCoordinates(dikuai),
      "点击查看/编辑基站坐标");
   contentPanel.add(baseStationPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
   JPanel boundaryOriginalPanel = createCardInfoItemWithIconButton("边界原始坐标:",
      createViewButton(e -> editBoundaryOriginalCoordinates(dikuai)));
   configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel),
      () -> editBoundaryOriginalCoordinates(dikuai),
      "点击查看/编辑边界原始坐标");
   contentPanel.add(boundaryOriginalPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
      JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:",
         getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"),
         createViewButton(e -> showCompletedMowingTrackDialog(dikuai)));
      setInfoItemTooltip(completedTrackPanel, dikuai.getMowingTrack());
      configureInteractiveLabel(getInfoItemTitleLabel(completedTrackPanel),
         () -> showCompletedMowingTrackDialog(dikuai),
         "点击查看完成的割草路径记录");
      contentPanel.add(completedTrackPanel);
      card.add(contentPanel, BorderLayout.CENTER);
      JButton deleteBtn = createDeleteButton();
      deleteBtn.addActionListener(e -> deleteDikuai(dikuai));
      JButton generatePathBtn = createPrimaryFooterButton("路径规划");
      generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai));
      JButton navigationPreviewBtn = createPrimaryFooterButton("导航预览");
      navigationPreviewBtn.addActionListener(e -> startNavigationPreview(dikuai));
      JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      footerPanel.setBackground(CARD_BACKGROUND);
      footerPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0));
      footerPanel.add(generatePathBtn);
      footerPanel.add(Box.createHorizontalStrut(12));
      footerPanel.add(navigationPreviewBtn);
      footerPanel.add(Box.createHorizontalStrut(12));
      footerPanel.add(deleteBtn);
      card.add(footerPanel, BorderLayout.SOUTH);
@@ -320,6 +418,7 @@
      
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(valueComp, BorderLayout.EAST);
      itemPanel.putClientProperty("titleLabel", labelComp);
      
      return itemPanel;
   }
@@ -327,7 +426,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));
@@ -335,13 +437,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);
@@ -349,40 +452,137 @@
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(rightPanel, BorderLayout.CENTER);
      itemPanel.putClientProperty("valueLabel", valueComp);
      itemPanel.putClientProperty("titleLabel", labelComp);
      
      return itemPanel;
   }
      private JPanel createBoundaryInfoItem(Dikuai dikuai, String displayValue) {
   private JPanel createCardInfoItemWithButton(String label, String value, JButton button) {
      JPanel itemPanel = new JPanel(new BorderLayout());
      itemPanel.setBackground(CARD_BACKGROUND);
      // 增加高度以确保按钮完整显示(按钮高度约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));
      labelComp.setForeground(LIGHT_TEXT);
      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);
      rightPanel.add(valueComp);
      rightPanel.add(button);
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(rightPanel, BorderLayout.CENTER);
      itemPanel.putClientProperty("valueLabel", valueComp);
      itemPanel.putClientProperty("titleLabel", labelComp);
      return itemPanel;
   }
   private JPanel createCardInfoItemWithButtonOnly(String label, String buttonText, ActionListener listener) {
      JPanel itemPanel = new JPanel(new BorderLayout());
      itemPanel.setBackground(CARD_BACKGROUND);
      // 增加高度以确保按钮完整显示(按钮高度约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));
      labelComp.setForeground(LIGHT_TEXT);
      JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
      rightPanel.setBackground(CARD_BACKGROUND);
      // 添加垂直内边距以确保按钮不被裁剪
      rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
      JButton button = createSmallLinkButton(buttonText, listener);
      rightPanel.add(button);
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(rightPanel, BorderLayout.CENTER);
      itemPanel.putClientProperty("titleLabel", labelComp);
      return itemPanel;
   }
   private JPanel createCardInfoItemWithIconButton(String label, JButton button) {
      JPanel itemPanel = new JPanel(new BorderLayout());
      itemPanel.setBackground(CARD_BACKGROUND);
      // 增加高度以确保按钮完整显示(按钮高度约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));
      labelComp.setForeground(LIGHT_TEXT);
      JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
      rightPanel.setBackground(CARD_BACKGROUND);
      // 添加垂直内边距以确保按钮不被裁剪
      rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
      rightPanel.add(button);
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(rightPanel, BorderLayout.CENTER);
      itemPanel.putClientProperty("titleLabel", labelComp);
      return itemPanel;
   }
      private JPanel createBoundaryInfoItem(Dikuai dikuai) {
         JPanel itemPanel = new JPanel(new BorderLayout());
         itemPanel.setBackground(CARD_BACKGROUND);
         int rowHeight = Math.max(36, BOUNDARY_TOGGLE_ICON_SIZE + 12);
         // 增加高度以确保按钮下边缘完整显示(按钮高度28,加上上下边距)
         int rowHeight = Math.max(30, BOUNDARY_TOGGLE_ICON_SIZE + 8);
         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, 28));
         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));
         JLabel valueComp = new JLabel(displayValue);
         valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
         valueComp.setForeground(TEXT_COLOR);
         // 状态提示文字标签
         JLabel statusLabel = new JLabel();
         statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
         statusLabel.setForeground(LIGHT_TEXT);
         JButton toggleButton = createBoundaryToggleButton(dikuai);
         // 将状态标签和按钮关联,以便在按钮状态变化时更新标签
         toggleButton.putClientProperty("statusLabel", statusLabel);
         rightPanel.add(valueComp);
         // 初始化状态文字
         String landNumber = dikuai.getLandNumber();
         boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false);
         updateBoundaryStatusLabel(statusLabel, isVisible);
         rightPanel.add(statusLabel);
         rightPanel.add(toggleButton);
         itemPanel.add(labelComp, BorderLayout.WEST);
         itemPanel.add(rightPanel, BorderLayout.CENTER);
         itemPanel.putClientProperty("valueLabel", valueComp);
         itemPanel.putClientProperty("titleLabel", labelComp);
         return itemPanel;
      }
@@ -396,7 +596,7 @@
         button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
         button.setMargin(new Insets(0, 0, 0, 0));
         button.setIconTextGap(0);
         button.setPreferredSize(new Dimension(56, 56));
         button.setPreferredSize(new Dimension(28, 28));
         String landNumber = dikuai.getLandNumber();
         boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false);
@@ -424,6 +624,24 @@
            button.setOpaque(true);
         }
         button.setToolTipText(active ? "隐藏边界点序号" : "显示边界点序号");
         // 更新状态提示文字
         Object statusLabelObj = button.getClientProperty("statusLabel");
         if (statusLabelObj instanceof JLabel) {
            JLabel statusLabel = (JLabel) statusLabelObj;
            updateBoundaryStatusLabel(statusLabel, active);
         }
      }
      private void updateBoundaryStatusLabel(JLabel statusLabel, boolean active) {
         if (statusLabel == null) {
            return;
         }
         if (active) {
            statusLabel.setText("已开启边界点显示");
         } else {
            statusLabel.setText("已关闭边界点显示");
         }
      }
      private void ensureBoundaryToggleIconsLoaded() {
@@ -466,6 +684,7 @@
               boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber);
               if (isCurrent) {
                  renderer.setBoundaryPointsVisible(desiredState);
                  renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d);
               }
            }
         }
@@ -481,6 +700,989 @@
      }
   }
   private JLabel getInfoItemTitleLabel(JPanel itemPanel) {
      if (itemPanel == null) {
         return null;
      }
      Object titleComp = itemPanel.getClientProperty("titleLabel");
      return titleComp instanceof JLabel ? (JLabel) titleComp : null;
   }
   private void configureInteractiveLabel(JLabel label, Runnable onClick, String tooltip) {
      if (label == null || onClick == null) {
         return;
      }
      Color originalColor = label.getForeground();
      label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      if (tooltip != null && !tooltip.trim().isEmpty()) {
         label.setToolTipText(tooltip);
      }
      label.addMouseListener(new MouseAdapter() {
         public void mouseClicked(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
               onClick.run();
            }
         }
         public void mouseEntered(MouseEvent e) {
            label.setForeground(PRIMARY_COLOR);
         }
         public void mouseExited(MouseEvent e) {
            label.setForeground(originalColor);
         }
      });
   }
   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 promptCoordinateEditing(String title, String initialValue) {
      return promptCoordinateEditing(title, initialValue, null);
   }
   private String promptCoordinateEditing(String title, String initialValue, Dikuai dikuai) {
      // 判断是否是往返点路径对话框
      boolean isReturnPathDialog = title != null && title.contains("往返点路径");
      if (isReturnPathDialog) {
         Window owner = SwingUtilities.getWindowAncestor(this);
         Wangfanpathpage page = new Wangfanpathpage(owner, title, initialValue, dikuai);
         page.setVisible(true);
         return page.getResult();
      }
      JTextArea textArea = new JTextArea(prepareCoordinateForEditor(initialValue));
      textArea.setLineWrap(true);
      textArea.setWrapStyleWord(true);
      textArea.setFont(new Font("微软雅黑", Font.PLAIN, 13));
      textArea.setCaretPosition(0);
      textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
      JScrollPane scrollPane = new JScrollPane(textArea);
      // 如果是往返点路径对话框,高度调整为适应两个文本域
      scrollPane.setPreferredSize(new Dimension(360, isReturnPathDialog ? 100 : 240));
      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);
      }
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      JPanel contentPanel = new JPanel();
      contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
      if (isReturnPathDialog) {
         contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
         // 减小边距以增加文本域宽度 (98%左右)
         contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
         // 1. 原始往返路径坐标区域
         String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getReturnPathRawCoordinates()) : "";
         int rawCount = 0;
         if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) {
            rawCount = rawCoords.split(";").length;
         }
         JPanel rawHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
         rawHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
         rawHeaderPanel.setBackground(BACKGROUND_COLOR);
         rawHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
         JLabel rawTitleLabel = new JLabel("原始往返路径坐标 (" + rawCount + "点)  ");
         rawTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
         rawHeaderPanel.add(rawTitleLabel);
         // 原始坐标复制按钮
         final String finalRawCoords = rawCoords;
         JButton rawCopyBtn = Fuzhibutton.createCopyButton(
            () -> {
               if (finalRawCoords == null || finalRawCoords.isEmpty() || "-1".equals(finalRawCoords)) return null;
               return finalRawCoords;
            },
            "复制",
            new Color(230, 250, 240)
         );
         rawCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
         rawCopyBtn.setPreferredSize(new Dimension(50, 24));
         rawCopyBtn.setMargin(new Insets(0,0,0,0));
         rawHeaderPanel.add(rawCopyBtn);
         contentPanel.add(rawHeaderPanel);
         contentPanel.add(Box.createVerticalStrut(5));
         JTextArea rawTextArea = new JTextArea(rawCoords);
         rawTextArea.setLineWrap(true);
         rawTextArea.setWrapStyleWord(true);
         rawTextArea.setFont(new Font("微软雅黑", Font.PLAIN, 13));
         rawTextArea.setEditable(false); // 原始坐标通常不可编辑
         rawTextArea.setRows(4);
         rawTextArea.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
         JScrollPane rawScroll = new JScrollPane(rawTextArea);
         rawScroll.setAlignmentX(Component.LEFT_ALIGNMENT);
         // 设置最大宽度允许扩展,首选宽度适中
         rawScroll.setPreferredSize(new Dimension(300, 100));
         rawScroll.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100));
         contentPanel.add(rawScroll);
         contentPanel.add(Box.createVerticalStrut(15));
         // 2. 优化后往返路径坐标区域
         String optCoords = prepareCoordinateForEditor(initialValue);
         int optCount = 0;
         if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) {
            optCount = optCoords.split(";").length;
         }
         JPanel optHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
         optHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
         optHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
         JLabel optTitleLabel = new JLabel("优化后往返路径坐标 (" + optCount + "点)  ");
         optTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
         optHeaderPanel.add(optTitleLabel);
         // 优化坐标复制按钮 - 动态获取文本域内容
         JButton optCopyBtn = Fuzhibutton.createCopyButton(
            () -> {
               String text = textArea.getText();
               if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) return null;
               return text;
            },
            "复制",
            new Color(230, 250, 240)
         );
         optCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
         optCopyBtn.setPreferredSize(new Dimension(50, 24));
         optCopyBtn.setMargin(new Insets(0,0,0,0));
         optHeaderPanel.add(optCopyBtn);
         contentPanel.add(optHeaderPanel);
         contentPanel.add(Box.createVerticalStrut(5));
         // 使用传入的 textArea (已初始化为 initialValue)
         textArea.setRows(4);
         scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
         scrollPane.setPreferredSize(new Dimension(300, 100));
         scrollPane.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100));
         contentPanel.add(scrollPane);
      } else {
         contentPanel.setLayout(new BorderLayout());
         contentPanel.add(scrollPane, BorderLayout.CENTER);
      }
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      JButton okButton;
      JButton cancelButton;
      JButton copyButton = null; // 初始化为null
      if (isReturnPathDialog) {
         // 往返点路径对话框:使用 buttonset 风格的确定按钮,图标按钮
         okButton = buttonset.createStyledButton("去绘制", new Color(70, 130, 220));
         // 取消按钮使用 closepage.png 图标
         cancelButton = new JButton();
         ImageIcon closeIcon = loadIcon("image/closepage.png", 25, 25);
         if (closeIcon != null) {
            cancelButton.setIcon(closeIcon);
         } else {
            cancelButton.setText("关闭");
         }
         cancelButton.setFont(new Font("微软雅黑", Font.PLAIN, 11));
         cancelButton.setForeground(PRIMARY_COLOR);
         cancelButton.setBorder(BorderFactory.createEmptyBorder());
         cancelButton.setContentAreaFilled(false);
         cancelButton.setFocusPainted(false);
         cancelButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
         cancelButton.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) { cancelButton.setOpaque(true); cancelButton.setBackground(new Color(255, 240, 240)); }
            public void mouseExited(MouseEvent e) { cancelButton.setOpaque(false); }
         });
         // 使用 Fuzhibutton 创建复制按钮 (这里不再需要底部的复制按钮,因为上面已经有了)
         // copyButton = ...
      } else {
         // 其他对话框保持原有样式
         okButton = new JButton("确定");
         cancelButton = new JButton("取消");
         copyButton = new JButton("复制");
         // 其他对话框的复制按钮逻辑
         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);
            }
         });
      }
      final boolean[] confirmed = new boolean[] {false};
      final String[] resultHolder = new String[1];
      okButton.addActionListener(e -> {
         if (isReturnPathDialog) {
            // 往返点路径对话框:标记为打开绘制页面
            // 如果文本域中已经有坐标,表示要重新绘制
            String currentText = textArea.getText();
            if (currentText != null && !currentText.trim().isEmpty() && !"-1".equals(currentText.trim())) {
               // 有坐标,表示重新绘制
               resultHolder[0] = "__OPEN_DRAW_PAGE_REFRESH__";
            } else {
               // 没有坐标,正常绘制
               resultHolder[0] = "__OPEN_DRAW_PAGE__";
            }
            confirmed[0] = true;
            dialog.dispose();
         } else {
            resultHolder[0] = textArea.getText();
            confirmed[0] = true;
            dialog.dispose();
         }
      });
      cancelButton.addActionListener(e -> dialog.dispose());
      buttonPanel.add(okButton);
      buttonPanel.add(cancelButton);
      if (copyButton != null) {
         buttonPanel.add(copyButton);
      }
      contentPanel.add(buttonPanel, isReturnPathDialog ? null : BorderLayout.SOUTH);
      if (isReturnPathDialog) {
         // 对于 BoxLayout,直接添加到底部
         JPanel bottomWrapper = new JPanel(new BorderLayout());
         bottomWrapper.add(buttonPanel, BorderLayout.EAST);
         contentPanel.add(bottomWrapper);
      }
      dialog.setContentPane(contentPanel);
      dialog.getRootPane().setDefaultButton(okButton);
      dialog.pack();
      // 如果是往返点路径对话框,设置宽度为首页的90%,高度保持不变
      if (isReturnPathDialog) {
         Shouye shouye = Shouye.getInstance();
         if (shouye != null && shouye.getWidth() > 0) {
            int homeWidth = shouye.getWidth();
            int dialogWidth = (int)(homeWidth * 0.9);
            Dimension currentSize = dialog.getSize();
            dialog.setSize(dialogWidth, currentSize.height);
         }
      }
      dialog.setLocationRelativeTo(this);
      dialog.setVisible(true);
      return confirmed[0] ? resultHolder[0] : null;
   }
   private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) {
      if (dikuai == null || fieldName == null || dikuai.getLandNumber() == null) {
         return false;
      }
      if (!Dikuai.updateField(dikuai.getLandNumber(), fieldName, value)) {
         return false;
      }
      Dikuai.updateField(dikuai.getLandNumber(), "updateTime", getCurrentTime());
      Dikuai.saveToProperties();
      boolean isCurrent = dikuai.getLandNumber().equals(currentWorkLandNumber);
      loadDikuaiData();
      if (isCurrent) {
         setCurrentWorkLand(dikuai.getLandNumber(), dikuai.getLandName());
      }
      return true;
   }
   private void editBoundaryCoordinates(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String edited = promptCoordinateEditing("查看 / 编辑地块边界坐标", dikuai.getBoundaryCoordinates());
      if (edited == null) {
         return;
      }
      String normalized = normalizeCoordinateInput(edited);
      if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", normalized)) {
         JOptionPane.showMessageDialog(this, "无法更新地块边界坐标", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      String message = "-1".equals(normalized) ? "地块边界坐标已清空" : "地块边界坐标已更新";
      JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   private void editPlannedPath(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String edited = promptCoordinateEditing("查看 / 编辑路径坐标", dikuai.getPlannedPath());
      if (edited == null) {
         return;
      }
      String normalized = normalizeCoordinateInput(edited);
      if (!saveFieldAndRefresh(dikuai, "plannedPath", normalized)) {
         JOptionPane.showMessageDialog(this, "无法更新路径坐标", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      String message = "-1".equals(normalized) ? "路径坐标已清空" : "路径坐标已更新";
      JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   private void editBaseStationCoordinates(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String edited = promptCoordinateEditing("查看 / 编辑基站坐标", dikuai.getBaseStationCoordinates());
      if (edited == null) {
         return;
      }
      String normalized = normalizeCoordinateInput(edited);
      if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", normalized)) {
         JOptionPane.showMessageDialog(this, "无法更新基站坐标", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      String message = "-1".equals(normalized) ? "基站坐标已清空" : "基站坐标已更新";
      JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   private void editBoundaryOriginalCoordinates(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String edited = promptCoordinateEditing("查看 / 编辑边界原始坐标", dikuai.getBoundaryOriginalCoordinates());
      if (edited == null) {
         return;
      }
      String normalized = normalizeCoordinateInput(edited);
      if (!saveFieldAndRefresh(dikuai, "boundaryOriginalCoordinates", normalized)) {
         JOptionPane.showMessageDialog(this, "无法更新边界原始坐标", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      String message = "-1".equals(normalized) ? "边界原始坐标已清空" : "边界原始坐标已更新";
      JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   private void editMowingPattern(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String current = sanitizeValueOrNull(dikuai.getMowingPattern());
      String normalized = normalizeExistingMowingPattern(current);
      JRadioButton parallelBtn = new JRadioButton("平行线 (parallel)");
      JRadioButton spiralBtn = new JRadioButton("螺旋形 (spiral)");
      ButtonGroup group = new ButtonGroup();
      group.add(parallelBtn);
      group.add(spiralBtn);
      if ("spiral".equals(normalized)) {
         spiralBtn.setSelected(true);
      } else {
         parallelBtn.setSelected(true);
      }
      JPanel panel = new JPanel();
      panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
      panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
      panel.add(new JLabel("请选择割草模式:"));
      panel.add(Box.createVerticalStrut(8));
      panel.add(parallelBtn);
      panel.add(Box.createVerticalStrut(4));
      panel.add(spiralBtn);
      int option = JOptionPane.showConfirmDialog(this, panel, "编辑割草模式", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
      if (option != JOptionPane.OK_OPTION) {
         return;
      }
      String selectedValue = parallelBtn.isSelected() ? "parallel" : "spiral";
      if (!saveFieldAndRefresh(dikuai, "mowingPattern", selectedValue)) {
         JOptionPane.showMessageDialog(this, "无法更新割草模式", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      JOptionPane.showMessageDialog(this, "割草模式已更新", "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   private String normalizeExistingMowingPattern(String value) {
      if (value == null) {
         return "parallel";
      }
      String trimmed = value.trim().toLowerCase();
      if (trimmed.isEmpty() || "-1".equals(trimmed)) {
         return "parallel";
      }
      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";
      }
   }
   private String sanitizeValueOrNull(String value) {
      if (value == null) {
         return null;
      }
      String trimmed = value.trim();
      if (trimmed.isEmpty() || "-1".equals(trimmed)) {
         return null;
      }
      return trimmed;
   }
   /**
    * 启动导航预览
    */
   private void startNavigationPreview(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      Window owner = SwingUtilities.getWindowAncestor(this);
      // 获取地块管理对话框,准备在打开导航预览时关闭
      Window managementWindow = null;
      if (owner instanceof JDialog) {
         managementWindow = owner;
      }
      // 关闭地块管理页面
      if (managementWindow != null) {
         managementWindow.dispose();
      }
      // 启动导航预览
      daohangyulan.getInstance().startNavigationPreview(dikuai);
   }
   /**
    * 显示路径规划页面
    */
   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;
      }
      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 initialGenerated = attemptMowingPathPreview(
         boundaryValue,
         obstacleValue,
         widthValue,
         modeValue,
         this,
         false
      );
      showMowingPathDialog(dikuai, baseStationValue, boundaryValue, obstacleValue, widthValue, modeValue, initialGenerated);
   }
   private void showMowingPathDialog(
      Dikuai dikuai,
      String baseStationValue,
      String boundaryValue,
      String obstacleValue,
      String widthValue,
      String modeValue,
      String initialGeneratedPath) {
      Window owner = SwingUtilities.getWindowAncestor(this);
      // 创建保存回调接口实现
      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,
         initialGeneratedPath,
         callback
      );
      dialog.setVisible(true);
   }
   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();
      String obstacles = sanitizeValueOrNull(obstacleInput);
      if (obstacles != null) {
         obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ');
      }
      String mode = normalizeExistingMowingPattern(modeInput);
      try {
         String generated = Lunjingguihua.generatePathFromStrings(boundary, obstacles, plannerWidth, mode);
         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);
         }
      }
      return null;
   }
   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);
      JLabel titleLabel = new JLabel(title);
      titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
      titleLabel.setForeground(TEXT_COLOR);
      section.add(titleLabel, 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);
      JLabel titleLabel = new JLabel(title);
      titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
      titleLabel.setForeground(TEXT_COLOR);
      section.add(titleLabel, 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 String formatMowingPatternForDisplay(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 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 List<Obstacledge.Obstacle> getConfiguredObstacles(Dikuai dikuai) {
      if (dikuai == null) {
         return Collections.emptyList();
      }
      List<Obstacledge.Obstacle> obstacles = loadObstaclesFromConfig(dikuai.getLandNumber());
      if (obstacles == null) {
         return Collections.emptyList();
      }
      return obstacles;
   }
   private String determineInitialObstacleValue(Dikuai dikuai, List<Obstacledge.Obstacle> configuredObstacles) {
      if (configuredObstacles != null && !configuredObstacles.isEmpty()) {
         String payload = Obstacledge.buildPlannerPayload(configuredObstacles);
         if (payload != null && !payload.trim().isEmpty()) {
            return payload;
         }
      }
      return "";
   }
   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 boolean persistObstaclesForLand(Dikuai dikuai, String baseStationValue, String obstaclePayload) {
      if (dikuai == null || dikuai.getLandNumber() == null) {
         return false;
      }
      String landNumber = dikuai.getLandNumber().trim();
      try {
         File configFile = new File("Obstacledge.properties");
         Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
         if (configFile.exists()) {
            if (!manager.loadFromFile(configFile.getAbsolutePath())) {
               return false;
            }
         }
         Obstacledge.Plot plot = manager.getPlotById(landNumber);
         if (plot == null) {
            plot = new Obstacledge.Plot(landNumber);
            manager.addPlot(plot);
         }
         applyBaseStationValue(plot, baseStationValue, dikuai.getBaseStationCoordinates());
         List<Obstacledge.Obstacle> obstacles;
         if (obstaclePayload == null || "-1".equals(obstaclePayload.trim())) {
            obstacles = new ArrayList<>();
         } else {
            obstacles = Obstacledge.parsePlannerPayload(obstaclePayload, landNumber);
         }
         plot.setObstacles(obstacles);
         if (!manager.saveToFile(configFile.getAbsolutePath())) {
            return false;
         }
         obstacleSummaryCache = loadObstacleSummaries();
         boolean isCurrent = landNumber.equals(currentWorkLandNumber);
         loadDikuaiData();
         if (isCurrent) {
            setCurrentWorkLand(landNumber, dikuai.getLandName());
         }
         return true;
      } catch (Exception ex) {
         System.err.println("保存障碍物配置失败: " + ex.getMessage());
         return false;
      }
   }
   private void applyBaseStationValue(Obstacledge.Plot plot, String baseStationValue, String fallbackValue) {
      if (plot == null) {
         return;
      }
      String sanitized = sanitizeBaseStationValue(baseStationValue);
      if (sanitized == null) {
         sanitized = sanitizeBaseStationValue(fallbackValue);
      }
      if (sanitized == null) {
         return;
      }
      try {
         plot.setBaseStationString(sanitized);
      } catch (Exception ex) {
         System.err.println("更新障碍物配置中的基站坐标失败: " + ex.getMessage());
      }
   }
   private String sanitizeBaseStationValue(String value) {
      if (value == null) {
         return null;
      }
      String trimmed = value.trim();
      if (trimmed.isEmpty() || "-1".equals(trimmed)) {
         return null;
      }
      return trimmed.replaceAll("\\s+", "");
   }
   private String getDisplayValue(String value, String defaultValue) {
      if (value == null || value.equals("-1") || value.trim().isEmpty()) {
         return defaultValue;
@@ -499,87 +1701,122 @@
      return value;
   }
   private JButton createSmallButton(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(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(PRIMARY_DARK);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(PRIMARY_COLOR);
         }
   /**
    * 创建类似于链接的小按钮
    */
   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;
   }
      return button;
   private JButton createSmallButton(String text) {
      return createSmallLinkButton(text, null);
   }
   private JButton createSmallButton(String text, Color backgroundColor, Color hoverColor) {
      // 对于需要不同颜色的按钮,使用实心风格
      Color baseColor = backgroundColor == null ? PRIMARY_COLOR : backgroundColor;
      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));
      ImageIcon deleteIcon = loadIcon("image/delete.png", 16, 16);
      JButton button = new JButton();
      ImageIcon deleteIcon = loadIcon("image/delete.png", 25, 25);
      if (deleteIcon != null) {
         button.setIcon(deleteIcon);
         button.setIconTextGap(6);
      } else {
         button.setText("删除");
      }
      // 悬停效果
      button.setFont(new Font("微软雅黑", Font.PLAIN, 11));
      button.setForeground(RED_COLOR);
      button.setBorder(BorderFactory.createEmptyBorder());
      button.setContentAreaFilled(false);
      button.setFocusPainted(false);
      button.setCursor(new Cursor(Cursor.HAND_CURSOR));
      button.addMouseListener(new MouseAdapter() {
         public void mouseEntered(MouseEvent e) {
            button.setBackground(RED_DARK);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(RED_COLOR);
         }
         public void mouseEntered(MouseEvent e) { button.setOpaque(true); button.setBackground(new Color(255, 240, 240)); }
         public void mouseExited(MouseEvent e) { button.setOpaque(false); }
      });
      return button;
   }
   private JButton createViewButton(ActionListener listener) {
      // 使用 Lookbutton 类创建查看按钮
      return Lookbutton.createViewButton(listener, new Color(230, 250, 240));
   }
   private JButton createPrimaryFooterButton(String text) {
      return createStyledButton(text, PRIMARY_COLOR, true); // 实心风格
   }
   private JButton createWorkToggleButton(Dikuai dikuai) {
      JButton button = new JButton();
      button.setContentAreaFilled(false);
@@ -607,6 +1844,13 @@
         button.setText(isCurrent ? "当前地块" : "设为当前");
      }
      button.setToolTipText(isCurrent ? "取消当前作业地块" : "设为当前作业地块");
      // 更新状态文字标签的显示/隐藏
      Object statusLabelObj = button.getClientProperty("statusLabel");
      if (statusLabelObj instanceof JLabel) {
         JLabel statusLabel = (JLabel) statusLabelObj;
         statusLabel.setVisible(isCurrent);
      }
   }
   private void ensureWorkIconsLoaded() {
@@ -644,10 +1888,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();
         }
@@ -658,22 +1911,34 @@
      Shouye shouye = Shouye.getInstance();
      if (shouye != null) {
         if (landNumber == null) {
         if (sanitizedLandNumber == null) {
            shouye.updateCurrentAreaName(null);
         } else {
            shouye.updateCurrentAreaName(landName);
         }
         MapRenderer renderer = shouye.getMapRenderer();
         if (renderer != null) {
            renderer.applyLandMetadata(dikuai);
            String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
            String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
            String obstacles = (dikuai != null) ? dikuai.getObstacleCoordinates() : null;
            renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
            List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList();
            if (configuredObstacles == null) {
               configuredObstacles = Collections.emptyList();
            }
            renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName);
            renderer.setCurrentPlannedPath(plannedPath);
            renderer.setCurrentObstacles(obstacles, 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);
      }
   }
@@ -681,6 +1946,62 @@
      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);
@@ -710,13 +2031,77 @@
      });
   }
   private void editReturnPoint(Dikuai dikuai) {
      FanhuiDialog fd = new FanhuiDialog(SwingUtilities.getWindowAncestor(this), dikuai);
      fd.setVisible(true);
      // 如果对话框已更新数据,刷新显示
      if (fd.isUpdated()) {
         loadDikuaiData();
   private void editReturnPath(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      String edited = promptCoordinateEditing("查看 / 编辑往返点路径", dikuai.getReturnPathCoordinates(), dikuai);
      if (edited == null) {
         return;
      }
      // 检查是否是特殊标记,表示点击了"去绘制"按钮
      if ("__OPEN_DRAW_PAGE__".equals(edited) || "__OPEN_DRAW_PAGE_REFRESH__".equals(edited)) {
         // 获取地块管理对话框
         Window owner = SwingUtilities.getWindowAncestor(this);
         Window managementWindow = null;
         if (owner instanceof JDialog) {
            managementWindow = owner;
         }
         // 获取地块管理对话框的父窗口(主窗口),作为绘制页面的父窗口
         Window drawPageOwner = null;
         if (managementWindow != null) {
            drawPageOwner = managementWindow.getOwner();
         }
         if (drawPageOwner == null && owner != null) {
            drawPageOwner = owner.getOwner();
         }
         if (drawPageOwner == null) {
            drawPageOwner = owner;
         }
         // 先关闭地块管理页面
         if (managementWindow != null) {
            managementWindow.dispose();
         }
         // 然后打开绘制页面,如果是重新绘制,传递标记
         boolean isRefresh = "__OPEN_DRAW_PAGE_REFRESH__".equals(edited);
         Huizhiwanfanpath.showDrawReturnPathDialog(drawPageOwner, dikuai, isRefresh);
         return;
      }
      String normalized = normalizeCoordinateInput(edited);
      if (!saveFieldAndRefresh(dikuai, "returnPathCoordinates", normalized)) {
         JOptionPane.showMessageDialog(this, "无法更新往返点路径", "错误", JOptionPane.ERROR_MESSAGE);
         return;
      }
      String message = "-1".equals(normalized) ? "往返点路径已清空" : "往返点路径已更新";
      JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
   }
   /**
    * 显示障碍物管理页面
    */
   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) {
@@ -734,39 +2119,177 @@
      loadDikuaiData();
   }
   private void showCompletedMowingTrackDialog(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      Window owner = SwingUtilities.getWindowAncestor(this);
      JDialog dialog = new JDialog(owner, "已完成的割草路径坐标", Dialog.ModalityType.APPLICATION_MODAL);
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      dialog.setLayout(new BorderLayout());
      dialog.getContentPane().setBackground(WHITE);
      String normalizedTrack = prepareCoordinateForEditor(dikuai.getMowingTrack());
      boolean hasTrack = normalizedTrack != null && !normalizedTrack.isEmpty();
      JTextArea textArea = new JTextArea(hasTrack ? normalizedTrack : "");
      textArea.setEditable(false);
      textArea.setLineWrap(true);
      textArea.setWrapStyleWord(true);
      textArea.setFont(new Font("微软雅黑", Font.PLAIN, 13));
      textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
      textArea.setCaretPosition(0);
   JScrollPane scrollPane = new JScrollPane(textArea);
   scrollPane.setPreferredSize(new Dimension(342, 240));
      scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12));
      JLabel statusLabel = new JLabel(hasTrack ? "当前已保存完成的割草路径记录。" : "当前暂无完成的割草路径记录。");
      statusLabel.setBorder(BorderFactory.createEmptyBorder(12, 12, 6, 12));
      statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
      statusLabel.setForeground(hasTrack ? TEXT_COLOR : LIGHT_TEXT);
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
      buttonPanel.setBackground(WHITE);
      buttonPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
      JButton deleteButton = createSmallButton("删除记录", RED_COLOR, RED_DARK);
      deleteButton.setEnabled(hasTrack);
      JButton closeButton = createSmallButton("关闭", PRIMARY_COLOR, PRIMARY_DARK);
      deleteButton.addActionListener(e -> {
         String latestTrack = prepareCoordinateForEditor(dikuai.getMowingTrack());
         if (latestTrack == null || latestTrack.isEmpty()) {
            JOptionPane.showMessageDialog(dialog, "当前没有可删除的轨迹记录。", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
         }
         int choice = JOptionPane.showConfirmDialog(dialog, "确定要删除已完成的割草路径记录吗?", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
         if (choice != JOptionPane.YES_OPTION) {
            return;
         }
         if (saveFieldAndRefresh(dikuai, "mowingTrack", "-1")) {
            dikuai.setMowingTrack("-1");
            textArea.setText("");
            statusLabel.setText("当前暂无完成的割草路径记录。");
            statusLabel.setForeground(LIGHT_TEXT);
            deleteButton.setEnabled(false);
            JOptionPane.showMessageDialog(dialog, "已删除完成路径记录", "成功", JOptionPane.INFORMATION_MESSAGE);
         } else {
            JOptionPane.showMessageDialog(dialog, "删除失败,请稍后重试。", "错误", JOptionPane.ERROR_MESSAGE);
         }
      });
      closeButton.addActionListener(e -> dialog.dispose());
      buttonPanel.add(deleteButton);
      buttonPanel.add(closeButton);
      dialog.add(statusLabel, BorderLayout.NORTH);
      dialog.add(scrollPane, BorderLayout.CENTER);
      dialog.add(buttonPanel, BorderLayout.SOUTH);
      dialog.pack();
   dialog.setMinimumSize(new Dimension(378, 320));
      dialog.setLocationRelativeTo(owner);
      dialog.setVisible(true);
   }
   private Map<String, ObstacleSummary> loadObstacleSummaries() {
      Map<String, ObstacleSummary> summaries = new HashMap<>();
      try {
         File configFile = new File("Obstacledge.properties");
         if (!configFile.exists()) {
            return summaries;
         }
         Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
         if (!manager.loadFromFile(configFile.getAbsolutePath())) {
            return summaries;
         }
         for (Obstacledge.Plot plot : manager.getPlots()) {
            if (plot == null) {
               continue;
            }
            String plotId = plot.getPlotId();
            if (plotId == null || plotId.trim().isEmpty()) {
               continue;
            }
            List<String> names = new ArrayList<>();
            for (Obstacledge.Obstacle obstacle : plot.getObstacles()) {
               if (obstacle == null) {
                  continue;
               }
               String name = obstacle.getObstacleName();
               if (name == null) {
                  continue;
               }
               String trimmed = name.trim();
               if (!trimmed.isEmpty()) {
                  names.add(trimmed);
               }
            }
            summaries.put(plotId.trim(), ObstacleSummary.of(names));
         }
      } catch (Exception ex) {
         System.err.println("读取障碍物配置失败: " + ex.getMessage());
      }
      return summaries;
   }
   private static List<Obstacledge.Obstacle> loadObstaclesFromConfig(String landNumber) {
      if (landNumber == null || landNumber.trim().isEmpty()) {
         return Collections.emptyList();
      }
      try {
         File configFile = new File("Obstacledge.properties");
         if (!configFile.exists()) {
            return null;
         }
         Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
         if (!manager.loadFromFile(configFile.getAbsolutePath())) {
            return null;
         }
         Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
         if (plot == null) {
            return Collections.emptyList();
         }
         List<Obstacledge.Obstacle> obstacles = plot.getObstacles();
         if (obstacles == null || obstacles.isEmpty()) {
            return Collections.emptyList();
         }
         return new ArrayList<>(obstacles);
      } catch (Exception ex) {
         System.err.println("读取障碍物配置失败: " + ex.getMessage());
         return null;
      }
   }
   private ObstacleSummary getObstacleSummaryFromCache(String landNumber) {
      if (landNumber == null || landNumber.trim().isEmpty()) {
         return ObstacleSummary.empty();
      }
      if (obstacleSummaryCache == null || obstacleSummaryCache.isEmpty()) {
         return ObstacleSummary.empty();
      }
      ObstacleSummary summary = obstacleSummaryCache.get(landNumber.trim());
      return summary != null ? summary : ObstacleSummary.empty();
   }
   private List<String> loadObstacleNamesForLand(String landNumber) {
      List<String> names = new ArrayList<>();
      if (landNumber == null || landNumber.trim().isEmpty()) {
         return names;
      }
      try {
         File configFile = new File("Obstacledge.properties");
         if (!configFile.exists()) {
            return names;
         }
         Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
         if (!manager.loadFromFile(configFile.getAbsolutePath())) {
            return names;
         }
         Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
         if (plot == null) {
            return names;
         }
         for (Obstacledge.Obstacle obstacle : plot.getObstacles()) {
            if (obstacle == null) {
               continue;
            }
            String name = obstacle.getObstacleName();
            if (name == null) {
               continue;
            }
            String trimmed = name.trim();
            if (!trimmed.isEmpty() && !names.contains(trimmed)) {
               names.add(trimmed);
            }
         }
      } catch (Exception ex) {
         System.err.println("读取障碍物配置失败: " + ex.getMessage());
      ObstacleSummary cached = getObstacleSummaryFromCache(landNumber);
      if (!cached.isEmpty()) {
         names.addAll(cached.copyNames());
         return names;
      }
      Map<String, ObstacleSummary> latest = loadObstacleSummaries();
      if (!latest.isEmpty()) {
         obstacleSummaryCache = latest;
      }
      ObstacleSummary refreshed = getObstacleSummaryFromCache(landNumber);
      if (!refreshed.isEmpty()) {
         names.addAll(refreshed.copyNames());
      }
      return names;
   }
@@ -887,4 +2410,70 @@
      }
      boundaryPointVisibility.put(landNumber, visible);
   }
   private static final class ObstacleSummary {
      private static final ObstacleSummary EMPTY = new ObstacleSummary(Collections.emptyList());
      private final List<String> names;
      private ObstacleSummary(List<String> names) {
         this.names = names;
      }
      static ObstacleSummary of(List<String> originalNames) {
         if (originalNames == null || originalNames.isEmpty()) {
            return empty();
         }
         List<String> cleaned = new ArrayList<>();
         for (String name : originalNames) {
            if (name == null) {
               continue;
            }
            String trimmed = name.trim();
            if (trimmed.isEmpty()) {
               continue;
            }
            boolean duplicated = false;
            for (String existing : cleaned) {
               if (existing.equalsIgnoreCase(trimmed)) {
                  duplicated = true;
                  break;
               }
            }
            if (!duplicated) {
               cleaned.add(trimmed);
            }
         }
         if (cleaned.isEmpty()) {
            return empty();
         }
         cleaned.sort(String::compareToIgnoreCase);
         return new ObstacleSummary(Collections.unmodifiableList(cleaned));
      }
      static ObstacleSummary empty() {
         return EMPTY;
      }
      boolean isEmpty() {
         return names.isEmpty();
      }
      int count() {
         return names.size();
      }
      String buildDisplayValue() {
         return count() > 0 ? String.format("障碍物%d个", count()) : "暂无障碍物";
      }
      String buildTooltip() {
         return count() > 0 ? String.join(",", names) : "暂无障碍物";
      }
      List<String> copyNames() {
         return new ArrayList<>(names);
      }
   }
}