张世豪
7 天以前 1cf1ecbc75c6d14b40efb3161e7db0b8b64f7de2
src/dikuai/Dikuaiguanli.java
@@ -6,6 +6,9 @@
import java.awt.datatransfer.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import ui.UIConfig;
@@ -17,8 +20,11 @@
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import lujing.Lunjingguihua;
import lujing.MowingPathGenerationPage;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
@@ -58,6 +64,8 @@
   private JButton addLandBtn;
   private static String currentWorkLandNumber;
   private static final String WORK_LAND_KEY = "currentWorkLandNumber";
   private static final String PROPERTIES_FILE = "set.properties";
   private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>();
   private ImageIcon workSelectedIcon;
   private ImageIcon workUnselectedIcon;
@@ -327,8 +335,8 @@
      JButton deleteBtn = createDeleteButton();
      deleteBtn.addActionListener(e -> deleteDikuai(dikuai));
      JButton generatePathBtn = createPrimaryFooterButton("生成割草路径");
      generatePathBtn.addActionListener(e -> generateMowingPath(dikuai));
      JButton generatePathBtn = createPrimaryFooterButton("路径规划");
      generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai));
      JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      footerPanel.setBackground(CARD_BACKGROUND);
@@ -504,6 +512,7 @@
               boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber);
               if (isCurrent) {
                  renderer.setBoundaryPointsVisible(desiredState);
                  renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d);
               }
            }
         }
@@ -583,17 +592,69 @@
      JScrollPane scrollPane = new JScrollPane(textArea);
      scrollPane.setPreferredSize(new Dimension(360, 240));
      int option = JOptionPane.showConfirmDialog(
         this,
         scrollPane,
         title,
         JOptionPane.OK_CANCEL_OPTION,
         JOptionPane.PLAIN_MESSAGE);
      if (option == JOptionPane.OK_OPTION) {
         return textArea.getText();
      Window owner = SwingUtilities.getWindowAncestor(this);
      JDialog dialog;
      if (owner instanceof Frame) {
         dialog = new JDialog((Frame) owner, title, true);
      } else if (owner instanceof Dialog) {
         dialog = new JDialog((Dialog) owner, title, true);
      } else {
         dialog = new JDialog((Frame) null, title, true);
      }
      return null;
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      JPanel contentPanel = new JPanel(new BorderLayout());
      contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
      contentPanel.add(scrollPane, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      JButton okButton = new JButton("确定");
      JButton cancelButton = new JButton("取消");
      JButton copyButton = new JButton("复制");
      final boolean[] confirmed = new boolean[] {false};
      final String[] resultHolder = new String[1];
      okButton.addActionListener(e -> {
         resultHolder[0] = textArea.getText();
         confirmed[0] = true;
         dialog.dispose();
      });
      cancelButton.addActionListener(e -> dialog.dispose());
      copyButton.addActionListener(e -> {
         String text = textArea.getText();
         if (text == null) {
            text = "";
         }
         String trimmed = text.trim();
         if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            JOptionPane.showMessageDialog(dialog, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
         }
         try {
            StringSelection selection = new StringSelection(text);
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(selection, selection);
            JOptionPane.showMessageDialog(dialog, title + " 已复制到剪贴板", "提示", JOptionPane.INFORMATION_MESSAGE);
         } catch (Exception ex) {
            JOptionPane.showMessageDialog(dialog, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
         }
      });
      buttonPanel.add(okButton);
      buttonPanel.add(cancelButton);
      buttonPanel.add(copyButton);
      contentPanel.add(buttonPanel, BorderLayout.SOUTH);
      dialog.setContentPane(contentPanel);
      dialog.getRootPane().setDefaultButton(okButton);
      dialog.pack();
      dialog.setLocationRelativeTo(this);
      dialog.setVisible(true);
      return confirmed[0] ? resultHolder[0] : null;
   }
   private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) {
@@ -733,10 +794,26 @@
      switch (trimmed) {
         case "1":
         case "spiral":
         case "螺旋":
         case "螺旋模式":
            return "spiral";
         case "0":
         case "parallel":
         case "平行":
         case "平行模式":
         default:
            if (trimmed.contains("螺旋")) {
               return "spiral";
            }
            if (trimmed.contains("spiral")) {
               return "spiral";
            }
            if (trimmed.contains("parallel")) {
               return "parallel";
            }
            if (trimmed.contains("平行")) {
               return "parallel";
            }
            return "parallel";
      }
   }
@@ -752,6 +829,77 @@
      return trimmed;
   }
   /**
    * 显示路径规划页面
    */
   private void showPathPlanningPage(Dikuai dikuai) {
      if (dikuai == null) {
         return;
      }
      Window owner = SwingUtilities.getWindowAncestor(this);
      // 获取地块基本数据
      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
      );
      dialog.setVisible(true);
   }
   private void generateMowingPath(Dikuai dikuai) {
      if (dikuai == null) {
         return;
@@ -790,178 +938,48 @@
      String modeValue,
      String initialGeneratedPath) {
      Window owner = SwingUtilities.getWindowAncestor(this);
      JDialog dialog = new JDialog(owner, "生成割草路径", Dialog.ModalityType.APPLICATION_MODAL);
      dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      dialog.getContentPane().setLayout(new BorderLayout());
      dialog.getContentPane().setBackground(BACKGROUND_COLOR);
      JPanel contentPanel = new JPanel();
      contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
      contentPanel.setBackground(BACKGROUND_COLOR);
      contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 16, 12, 16));
      String landName = getDisplayValue(dikuai.getLandName(), "未知地块");
      String landNumber = getDisplayValue(dikuai.getLandNumber(), "未知编号");
      JLabel headerLabel = new JLabel(landName + " / " + landNumber);
      headerLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
      headerLabel.setForeground(TEXT_COLOR);
      headerLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
      contentPanel.add(headerLabel);
      contentPanel.add(Box.createVerticalStrut(12));
      JTextField baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true);
      contentPanel.add(createTextFieldSection("基站坐标", baseStationField));
      JTextArea boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6);
      contentPanel.add(createTextAreaSection("地块边界", boundaryArea));
      JTextArea obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6);
      contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea));
      JTextField widthField = createInfoTextField(widthValue != null ? widthValue : "", true);
      contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField));
      contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue)));
      String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
      String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath;
      JTextArea pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10);
      contentPanel.add(createTextAreaSection("割草路径坐标", pathArea));
      JScrollPane dialogScrollPane = new JScrollPane(contentPanel);
      dialogScrollPane.setBorder(BorderFactory.createEmptyBorder());
      dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16);
      dialog.add(dialogScrollPane, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12));
      buttonPanel.setBackground(BACKGROUND_COLOR);
      JButton generateBtn = createPrimaryFooterButton("生成割草路径");
      JButton saveBtn = createPrimaryFooterButton("保存路径");
      JButton cancelBtn = createPrimaryFooterButton("取消");
      generateBtn.addActionListener(e -> {
         String sanitizedWidth = sanitizeWidthString(widthField.getText());
         if (sanitizedWidth != null) {
            try {
               double widthCm = Double.parseDouble(sanitizedWidth);
               widthField.setText(formatWidthForStorage(widthCm));
               sanitizedWidth = formatWidthForStorage(widthCm);
            } catch (NumberFormatException ex) {
               widthField.setText(sanitizedWidth);
            }
      // 创建保存回调接口实现
      MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() {
         @Override
         public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value);
         }
         String generated = attemptMowingPathPreview(
            boundaryArea.getText(),
            obstacleArea.getText(),
            sanitizedWidth,
            modeValue,
            dialog,
            true
         );
         if (generated != null) {
            pathArea.setText(generated);
            pathArea.setCaretPosition(0);
         @Override
         public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value);
         }
      });
      saveBtn.addActionListener(e -> {
         String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText());
         String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText());
         if (!"-1".equals(boundaryNormalized)) {
            boundaryNormalized = boundaryNormalized
               .replace("\r\n", ";")
               .replace('\r', ';')
               .replace('\n', ';')
               .replaceAll(";+", ";")
               .replaceAll("\\s*;\\s*", ";")
               .trim();
            if (boundaryNormalized.isEmpty()) {
               boundaryNormalized = "-1";
            }
         @Override
         public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) {
            return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue);
         }
         String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText());
         if (!"-1".equals(obstacleNormalized)) {
            obstacleNormalized = obstacleNormalized
               .replace("\r\n", " ")
               .replace('\r', ' ')
               .replace('\n', ' ')
               .replaceAll("\\s{2,}", " ")
               .trim();
            if (obstacleNormalized.isEmpty()) {
               obstacleNormalized = "-1";
            }
         @Override
         public boolean saveMowingWidth(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "mowingWidth", value);
         }
         String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
         String widthSanitized = sanitizeWidthString(widthField.getText());
         if (widthSanitized == null) {
            String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
            JOptionPane.showMessageDialog(dialog, message, "提示", JOptionPane.WARNING_MESSAGE);
            return;
         @Override
         public boolean savePlannedPath(Dikuai dikuai, String value) {
            return saveFieldAndRefresh(dikuai, "plannedPath", value);
         }
         double widthCm;
         try {
            widthCm = Double.parseDouble(widthSanitized);
         } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(dialog, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE);
            return;
         }
         if (widthCm <= 0) {
            JOptionPane.showMessageDialog(dialog, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            return;
         }
         String widthNormalized = formatWidthForStorage(widthCm);
         widthField.setText(widthNormalized);
         String pathNormalized = normalizeCoordinateInput(pathArea.getText());
         if (!"-1".equals(pathNormalized)) {
            pathNormalized = pathNormalized
               .replace("\r\n", ";")
               .replace('\r', ';')
               .replace('\n', ';')
               .replaceAll(";+", ";")
               .replaceAll("\\s*;\\s*", ";")
               .trim();
            if (pathNormalized.isEmpty()) {
               pathNormalized = "-1";
            }
         }
         if ("-1".equals(pathNormalized)) {
            JOptionPane.showMessageDialog(dialog, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", baseStationNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存基站坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", boundaryNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存地块边界", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!persistObstaclesForLand(dikuai, baseStationNormalized, obstacleNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "mowingWidth", widthNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存割草宽度", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         if (!saveFieldAndRefresh(dikuai, "plannedPath", pathNormalized)) {
            JOptionPane.showMessageDialog(dialog, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE);
            return;
         }
         JOptionPane.showMessageDialog(dialog, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
         dialog.dispose();
      });
      cancelBtn.addActionListener(e -> dialog.dispose());
      buttonPanel.add(generateBtn);
      buttonPanel.add(saveBtn);
      buttonPanel.add(cancelBtn);
      dialog.add(buttonPanel, BorderLayout.SOUTH);
      dialog.pack();
      dialog.setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
      dialog.setLocationRelativeTo(owner);
      };
      // 使用新的独立页面类
      MowingPathGenerationPage dialog = new MowingPathGenerationPage(
         owner,
         dikuai,
         baseStationValue,
         boundaryValue,
         obstacleValue,
         widthValue,
         modeValue,
         initialGeneratedPath,
         callback
      );
      dialog.setVisible(true);
   }
@@ -1446,10 +1464,19 @@
   }
   public static void setCurrentWorkLand(String landNumber, String landName) {
      currentWorkLandNumber = landNumber;
      String sanitizedLandNumber = sanitizeLandNumber(landNumber);
      boolean changed = !Objects.equals(currentWorkLandNumber, sanitizedLandNumber);
      if (!changed) {
         String persisted = readPersistedWorkLandNumber();
         if (!Objects.equals(persisted, sanitizedLandNumber)) {
            changed = true;
         }
      }
      currentWorkLandNumber = sanitizedLandNumber;
      Dikuai dikuai = null;
      if (landNumber != null) {
         dikuai = Dikuai.getDikuai(landNumber);
      if (sanitizedLandNumber != null) {
         dikuai = Dikuai.getDikuai(sanitizedLandNumber);
         if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) {
            landName = dikuai.getLandName();
         }
@@ -1460,7 +1487,7 @@
      Shouye shouye = Shouye.getInstance();
      if (shouye != null) {
         if (landNumber == null) {
         if (sanitizedLandNumber == null) {
            shouye.updateCurrentAreaName(null);
         } else {
            shouye.updateCurrentAreaName(landName);
@@ -1470,24 +1497,85 @@
            renderer.applyLandMetadata(dikuai);
            String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
            String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
            List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : Collections.emptyList();
            List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList();
            if (configuredObstacles == null) {
               configuredObstacles = Collections.emptyList();
            }
            renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
            renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName);
            renderer.setCurrentPlannedPath(plannedPath);
            renderer.setCurrentObstacles(configuredObstacles, landNumber);
            boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false);
            renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber);
            boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false);
            renderer.setBoundaryPointsVisible(showBoundaryPoints);
            renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d);
         }
         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);