张世豪
2025-12-09 32524195d474b74e48916867b2a6c2f022a40d98
src/dikuai/Dikuaiguanli.java
@@ -6,7 +6,11 @@
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;
import ui.UIUtils;
import java.text.SimpleDateFormat;
@@ -16,7 +20,10 @@
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import lujing.Lunjingguihua;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
@@ -56,6 +63,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;
@@ -242,6 +251,9 @@
      JPanel boundaryPanel = createBoundaryInfoItem(dikuai,
         getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "未设置"));
      setInfoItemTooltip(boundaryPanel, dikuai.getBoundaryCoordinates());
      configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel),
         () -> editBoundaryCoordinates(dikuai),
         "点击查看/编辑地块边界坐标");
      contentPanel.add(boundaryPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      
@@ -259,6 +271,9 @@
         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)));
@@ -266,6 +281,9 @@
         getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("基站坐标", dikuai.getBaseStationCoordinates()));
      setInfoItemTooltip(baseStationPanel, dikuai.getBaseStationCoordinates());
      configureInteractiveLabel(getInfoItemTitleLabel(baseStationPanel),
         () -> editBaseStationCoordinates(dikuai),
         "点击查看/编辑基站坐标");
      contentPanel.add(baseStationPanel);
   contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
@@ -273,6 +291,9 @@
         getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("边界原始坐标", dikuai.getBoundaryOriginalCoordinates()));
      setInfoItemTooltip(boundaryOriginalPanel, dikuai.getBoundaryOriginalCoordinates());
      configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel),
         () -> editBoundaryOriginalCoordinates(dikuai),
         "点击查看/编辑边界原始坐标");
      contentPanel.add(boundaryOriginalPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
@@ -280,6 +301,9 @@
         getTruncatedValue(dikuai.getMowingPattern(), 12, "未设置"),
         "复制", e -> copyCoordinatesAction("割草模式", dikuai.getMowingPattern()));
      setInfoItemTooltip(mowingPatternPanel, dikuai.getMowingPattern());
      configureInteractiveLabel(getInfoItemTitleLabel(mowingPatternPanel),
         () -> editMowingPattern(dikuai),
         "点击查看/编辑割草模式");
      contentPanel.add(mowingPatternPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
@@ -294,15 +318,30 @@
         "编辑", e -> editMowingWidth(dikuai));
      setInfoItemTooltip(mowingWidthPanel, widthSource);
      contentPanel.add(mowingWidthPanel);
      contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
      JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:",
         getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"),
         "查看", 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 -> generateMowingPath(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(deleteBtn);
      card.add(footerPanel, BorderLayout.SOUTH);
@@ -353,6 +392,7 @@
      itemPanel.add(labelComp, BorderLayout.WEST);
      itemPanel.add(rightPanel, BorderLayout.CENTER);
      itemPanel.putClientProperty("valueLabel", valueComp);
      itemPanel.putClientProperty("titleLabel", labelComp);
      
      return itemPanel;
   }
@@ -387,6 +427,7 @@
         itemPanel.add(labelComp, BorderLayout.WEST);
         itemPanel.add(rightPanel, BorderLayout.CENTER);
         itemPanel.putClientProperty("valueLabel", valueComp);
         itemPanel.putClientProperty("titleLabel", labelComp);
         return itemPanel;
      }
@@ -470,6 +511,7 @@
               boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber);
               if (isCurrent) {
                  renderer.setBoundaryPointsVisible(desiredState);
                  renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d);
               }
            }
         }
@@ -485,6 +527,811 @@
      }
   }
   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) {
      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, 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(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) {
      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 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);
      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);
            }
         }
         String generated = attemptMowingPathPreview(
            boundaryArea.getText(),
            obstacleArea.getText(),
            sanitizedWidth,
            modeValue,
            dialog,
            true
         );
         if (generated != null) {
            pathArea.setText(generated);
            pathArea.setCaretPosition(0);
         }
      });
      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";
            }
         }
         String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText());
         if (!"-1".equals(obstacleNormalized)) {
            obstacleNormalized = obstacleNormalized
               .replace("\r\n", " ")
               .replace('\r', ' ')
               .replace('\n', ' ')
               .replaceAll("\\s{2,}", " ")
               .trim();
            if (obstacleNormalized.isEmpty()) {
               obstacleNormalized = "-1";
            }
         }
         String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
         String widthSanitized = sanitizeWidthString(widthField.getText());
         if (widthSanitized == null) {
            String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
            JOptionPane.showMessageDialog(dialog, message, "提示", JOptionPane.WARNING_MESSAGE);
            return;
         }
         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);
      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 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;
@@ -504,22 +1351,27 @@
   }
   private JButton createSmallButton(String text) {
      return createSmallButton(text, PRIMARY_COLOR, PRIMARY_DARK);
   }
   private JButton createSmallButton(String text, Color backgroundColor, Color hoverColor) {
      JButton button = new JButton(text);
      button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
      button.setBackground(PRIMARY_COLOR);
      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(PRIMARY_DARK);
            button.setBackground(hover);
         }
         public void mouseExited(MouseEvent e) {
            button.setBackground(PRIMARY_COLOR);
            button.setBackground(baseColor);
         }
      });
@@ -584,6 +1436,28 @@
      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;
   }
   private JButton createWorkToggleButton(Dikuai dikuai) {
      JButton button = new JButton();
      button.setContentAreaFilled(false);
@@ -648,10 +1522,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();
         }
@@ -662,7 +1545,7 @@
      Shouye shouye = Shouye.getInstance();
      if (shouye != null) {
         if (landNumber == null) {
         if (sanitizedLandNumber == null) {
            shouye.updateCurrentAreaName(null);
         } else {
            shouye.updateCurrentAreaName(landName);
@@ -672,26 +1555,85 @@
            renderer.applyLandMetadata(dikuai);
            String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
            String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
            String obstacles = (dikuai != null) ? dikuai.getObstacleCoordinates() : null;
            List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : null;
            renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
            renderer.setCurrentPlannedPath(plannedPath);
            if (configuredObstacles != null) {
               renderer.setCurrentObstacles(configuredObstacles, landNumber);
            } else {
               renderer.setCurrentObstacles(obstacles, landNumber);
            List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList();
            if (configuredObstacles == null) {
               configuredObstacles = Collections.emptyList();
            }
            boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false);
            renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName);
            renderer.setCurrentPlannedPath(plannedPath);
            renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber);
            boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false);
            renderer.setBoundaryPointsVisible(showBoundaryPoints);
            renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d);
         }
         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);
@@ -745,6 +1687,80 @@
      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 {