package dikuai; import javax.swing.*; import java.awt.*; import java.awt.event.*; 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; 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 zhangaiwu.AddDikuai; import zhangaiwu.Obstacledge; import zhuye.MapRenderer; import zhuye.Shouye; import zhuye.Coordinate; /** * 地块管理面板 - 卡片式布局设计 * 改为JPanel以适应主界面嵌入,参考Shouye尺寸 */ public class Dikuaiguanli extends JPanel { private static final long serialVersionUID = 1L; private static Dikuaiguanli latestInstance; // 6.5寸竖屏尺寸 private final int SCREEN_WIDTH = 400; private final int SCREEN_HEIGHT = 800; // 主题颜色 private final Color PRIMARY_COLOR = new Color(46, 139, 87); private final Color PRIMARY_DARK = new Color(30, 107, 69); private final Color RED_COLOR = new Color(255, 107, 107); private final Color RED_DARK = new Color(255, 82, 82); private final Color TEXT_COLOR = new Color(51, 51, 51); private final Color LIGHT_TEXT = new Color(119, 119, 119); private final Color WHITE = Color.WHITE; private final Color BORDER_COLOR = new Color(200, 200, 200); private final Color BACKGROUND_COLOR = new Color(250, 250, 250); private final Color CARD_BACKGROUND = new Color(255, 255, 255); private final Color CARD_SHADOW = new Color(220, 220, 220); // 组件 private JPanel mainPanel; private JPanel cardsPanel; private JScrollPane scrollPane; // 按钮 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 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 Map obstacleSummaryCache = Collections.emptyMap(); public Dikuaiguanli(String landNumber) { latestInstance = this; initializeUI(landNumber); setupEventHandlers(); } private void copyCoordinatesAction(String title, String coordinates) { if (coordinates == null || coordinates.equals("-1") || coordinates.trim().isEmpty()) { JOptionPane.showMessageDialog(this, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE); return; } try { StringSelection selection = new StringSelection(coordinates); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(selection, selection); JOptionPane.showMessageDialog(this, title + " 已复制到剪贴板", "提示", JOptionPane.INFORMATION_MESSAGE); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } private void initializeUI(String landNumber) { setLayout(new BorderLayout()); setBackground(BACKGROUND_COLOR); // 使用与Shouye相同的6.5寸竖屏尺寸 setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT)); createMainPanel(); // 添加到主面板 add(mainPanel, BorderLayout.CENTER); // 加载地块数据 loadDikuaiData(); } private void createMainPanel() { mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); mainPanel.setBackground(BACKGROUND_COLOR); mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); // 顶部:新增按钮 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); topPanel.setBackground(BACKGROUND_COLOR); addLandBtn = createActionButton("+ 新增地块", PRIMARY_COLOR); addLandBtn.setPreferredSize(new Dimension(120, 40)); addLandBtn.setFont(new Font("微软雅黑", Font.BOLD, 14)); topPanel.add(addLandBtn); mainPanel.add(topPanel, BorderLayout.NORTH); // 中部:卡片区域 cardsPanel = new JPanel(); cardsPanel.setLayout(new BoxLayout(cardsPanel, BoxLayout.Y_AXIS)); cardsPanel.setBackground(BACKGROUND_COLOR); scrollPane = new JScrollPane(cardsPanel); scrollPane.setBorder(BorderFactory.createEmptyBorder()); scrollPane.getVerticalScrollBar().setUnitIncrement(16); scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); mainPanel.add(scrollPane, BorderLayout.CENTER); } private void loadDikuaiData() { Dikuai.initFromProperties(); // 清空现有卡片 cardsPanel.removeAll(); Map 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); } // 为每个地块创建卡片 for (Dikuai dikuai : allDikuai.values()) { JPanel card = createDikuaiCard(dikuai); cardsPanel.add(card); cardsPanel.add(Box.createRigidArea(new Dimension(0, 15))); } } cardsPanel.revalidate(); cardsPanel.repaint(); } private JPanel createEmptyStatePanel() { JPanel emptyPanel = new JPanel(); emptyPanel.setLayout(new BorderLayout()); emptyPanel.setBackground(BACKGROUND_COLOR); emptyPanel.setBorder(BorderFactory.createEmptyBorder(50, 0, 0, 0)); JLabel emptyLabel = new JLabel("暂无地块数据", JLabel.CENTER); emptyLabel.setFont(new Font("微软雅黑", Font.PLAIN, 16)); emptyLabel.setForeground(LIGHT_TEXT); emptyPanel.add(emptyLabel, BorderLayout.CENTER); return emptyPanel; } private JPanel createDikuaiCard(Dikuai dikuai) { JPanel card = new JPanel(); card.setLayout(new BorderLayout()); card.setBackground(CARD_BACKGROUND); card.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(CARD_SHADOW, 1), BorderFactory.createEmptyBorder(15, 15, 15, 15) )); card.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); // 卡片头部:地块名称和删除按钮 JPanel headerPanel = new JPanel(new BorderLayout()); headerPanel.setBackground(CARD_BACKGROUND); // 地块名称 String landName = dikuai.getLandName(); if (landName == null || landName.equals("-1")) { landName = "未知地块"; } JLabel nameLabel = new JLabel(landName); nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); nameLabel.setForeground(TEXT_COLOR); headerPanel.add(nameLabel, BorderLayout.WEST); JButton workToggleBtn = createWorkToggleButton(dikuai); headerPanel.add(workToggleBtn, BorderLayout.EAST); card.add(headerPanel, BorderLayout.NORTH); // 卡片内容:地块信息 JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); contentPanel.setBackground(CARD_BACKGROUND); contentPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); // 地块编号 contentPanel.add(createCardInfoItem("地块编号:", getDisplayValue(dikuai.getLandNumber(), "未知"))); contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 添加时间 contentPanel.add(createCardInfoItem("添加时间:", getDisplayValue(dikuai.getCreateTime(), "未知"))); contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 地块面积 String landArea = dikuai.getLandArea(); if (landArea != null && !landArea.equals("-1")) { landArea += "㎡"; } else { 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()); configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel), () -> editBoundaryCoordinates(dikuai), "点击查看/编辑地块边界坐标"); contentPanel.add(boundaryPanel); contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber()); JPanel obstaclePanel = createCardInfoItemWithButton("障碍物:", obstacleSummary.buildDisplayValue(), "新增", e -> addNewObstacle(dikuai)); setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip()); contentPanel.add(obstaclePanel); contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 路径坐标(带查看按钮) JPanel pathPanel = createCardInfoItemWithButton("路径坐标:", getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"), "复制", e -> copyCoordinatesAction("路径坐标", dikuai.getPlannedPath())); setInfoItemTooltip(pathPanel, dikuai.getPlannedPath()); configureInteractiveLabel(getInfoItemTitleLabel(pathPanel), () -> editPlannedPath(dikuai), "点击查看/编辑路径坐标"); contentPanel.add(pathPanel); contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); JPanel baseStationPanel = createCardInfoItemWithButton("基站坐标:", 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))); JPanel boundaryOriginalPanel = createCardInfoItemWithButton("边界原始坐标:", 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))); JPanel mowingPatternPanel = createCardInfoItemWithButton("割草模式:", 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))); String mowingWidthValue = dikuai.getMowingWidth(); String widthSource = null; if (mowingWidthValue != null && !"-1".equals(mowingWidthValue) && !mowingWidthValue.trim().isEmpty()) { widthSource = mowingWidthValue + "厘米"; } String displayWidth = getTruncatedValue(widthSource, 12, "未设置"); JPanel mowingWidthPanel = createCardInfoItemWithButton("割草宽度:", displayWidth, "编辑", 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 -> showPathPlanningPage(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); return card; } private JPanel createCardInfoItem(String label, String value) { JPanel itemPanel = new JPanel(new BorderLayout()); itemPanel.setBackground(CARD_BACKGROUND); itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); JLabel labelComp = new JLabel(label); labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); labelComp.setForeground(LIGHT_TEXT); JLabel valueComp = new JLabel(value); valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); valueComp.setForeground(TEXT_COLOR); itemPanel.add(labelComp, BorderLayout.WEST); itemPanel.add(valueComp, BorderLayout.EAST); return itemPanel; } 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)); 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); JLabel valueComp = new JLabel(value); valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); valueComp.setForeground(TEXT_COLOR); JButton button = createSmallButton(buttonText); button.addActionListener(listener); 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 createBoundaryInfoItem(Dikuai dikuai, String displayValue) { JPanel itemPanel = new JPanel(new BorderLayout()); itemPanel.setBackground(CARD_BACKGROUND); int rowHeight = Math.max(36, BOUNDARY_TOGGLE_ICON_SIZE + 12); Dimension rowDimension = new Dimension(Integer.MAX_VALUE, rowHeight); itemPanel.setMaximumSize(rowDimension); itemPanel.setPreferredSize(rowDimension); itemPanel.setMinimumSize(new Dimension(0, 32)); 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); 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); JButton toggleButton = createBoundaryToggleButton(dikuai); rightPanel.add(valueComp); rightPanel.add(toggleButton); itemPanel.add(labelComp, BorderLayout.WEST); itemPanel.add(rightPanel, BorderLayout.CENTER); itemPanel.putClientProperty("valueLabel", valueComp); itemPanel.putClientProperty("titleLabel", labelComp); return itemPanel; } private JButton createBoundaryToggleButton(Dikuai dikuai) { JButton button = new JButton(); button.setContentAreaFilled(false); button.setBorder(BorderFactory.createEmptyBorder()); button.setFocusPainted(false); button.setOpaque(false); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.setMargin(new Insets(0, 0, 0, 0)); button.setIconTextGap(0); button.setPreferredSize(new Dimension(56, 56)); String landNumber = dikuai.getLandNumber(); boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false); updateBoundaryToggleButton(button, isVisible); button.addActionListener(e -> toggleBoundaryPointVisualization(dikuai, button)); return button; } private void updateBoundaryToggleButton(JButton button, boolean active) { ensureBoundaryToggleIconsLoaded(); ImageIcon icon = active ? boundaryVisibleIcon : boundaryHiddenIcon; if (icon != null) { button.setIcon(icon); button.setText(null); button.setContentAreaFilled(false); button.setOpaque(false); } else { button.setIcon(null); button.setText(active ? "关闭" : "开启"); button.setContentAreaFilled(true); button.setBackground(PRIMARY_COLOR); button.setForeground(WHITE); button.setOpaque(true); } button.setToolTipText(active ? "隐藏边界点序号" : "显示边界点序号"); } private void ensureBoundaryToggleIconsLoaded() { if (boundaryVisibleIcon == null) { boundaryVisibleIcon = loadIcon("image/open.png", BOUNDARY_TOGGLE_ICON_SIZE, BOUNDARY_TOGGLE_ICON_SIZE); } if (boundaryHiddenIcon == null) { boundaryHiddenIcon = loadIcon("image/close.png", BOUNDARY_TOGGLE_ICON_SIZE, BOUNDARY_TOGGLE_ICON_SIZE); } } private void toggleBoundaryPointVisualization(Dikuai dikuai, JButton button) { if (dikuai == null) { return; } String landNumber = dikuai.getLandNumber(); if (landNumber == null || landNumber.trim().isEmpty()) { return; } boolean currentState = boundaryPointVisibility.getOrDefault(landNumber, false); boolean desiredState = !currentState; if (desiredState) { String boundary = dikuai.getBoundaryCoordinates(); if (boundary == null || boundary.trim().isEmpty() || "-1".equals(boundary.trim())) { JOptionPane.showMessageDialog(this, "当前地块暂无边界数据", "提示", JOptionPane.INFORMATION_MESSAGE); desiredState = false; } } boundaryPointVisibility.put(landNumber, desiredState); updateBoundaryToggleButton(button, desiredState); Shouye shouye = Shouye.getInstance(); if (shouye != null) { MapRenderer renderer = shouye.getMapRenderer(); if (renderer != null) { boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber); if (isCurrent) { renderer.setBoundaryPointsVisible(desiredState); renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d); } } } } private void setInfoItemTooltip(JPanel itemPanel, String rawValue) { if (itemPanel == null || rawValue == null || rawValue.trim().isEmpty() || "-1".equals(rawValue.trim())) { return; } Object valueComp = itemPanel.getClientProperty("valueLabel"); if (valueComp instanceof JLabel) { ((JLabel) valueComp).setToolTipText(rawValue); } } 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 showPathPlanningPage(Dikuai dikuai) { if (dikuai == null) { return; } Window owner = SwingUtilities.getWindowAncestor(this); // 获取地块基本数据 String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates()); String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates()); List 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; } String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates()); String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates()); List 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 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 getConfiguredObstacles(Dikuai dikuai) { if (dikuai == null) { return Collections.emptyList(); } List obstacles = loadObstaclesFromConfig(dikuai.getLandNumber()); if (obstacles == null) { return Collections.emptyList(); } return obstacles; } private String determineInitialObstacleValue(Dikuai dikuai, List 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 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; } return value; } private String getTruncatedValue(String value, int maxLength, String defaultValue) { if (value == null || value.equals("-1") || value.trim().isEmpty()) { return defaultValue; } if (value.length() > maxLength) { return value.substring(0, maxLength) + "..."; } return value; } 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)); Color baseColor = backgroundColor == null ? PRIMARY_COLOR : backgroundColor; Color hover = hoverColor == null ? baseColor : hoverColor; button.setBackground(baseColor); button.setForeground(WHITE); button.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10)); button.setMargin(new Insets(0, 0, 0, 0)); button.setFocusPainted(false); button.setCursor(new Cursor(Cursor.HAND_CURSOR)); button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { button.setBackground(hover); } public void mouseExited(MouseEvent e) { button.setBackground(baseColor); } }); return button; } 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)); // 悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { if (color == RED_COLOR) { button.setBackground(RED_DARK); } else { button.setBackground(PRIMARY_DARK); } } public void mouseExited(MouseEvent e) { if (color == RED_COLOR) { button.setBackground(RED_COLOR); } else { button.setBackground(PRIMARY_COLOR); } } }); return button; } 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); if (deleteIcon != null) { button.setIcon(deleteIcon); button.setIconTextGap(6); } // 悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { button.setBackground(RED_DARK); } public void mouseExited(MouseEvent e) { button.setBackground(RED_COLOR); } }); return button; } private JButton createPrimaryFooterButton(String text) { JButton button = new JButton(text); button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); button.setBackground(PRIMARY_COLOR); button.setForeground(WHITE); button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12)); button.setFocusPainted(false); button.setCursor(new Cursor(Cursor.HAND_CURSOR)); button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { button.setBackground(PRIMARY_DARK); } public void mouseExited(MouseEvent e) { button.setBackground(PRIMARY_COLOR); } }); return button; } private JButton createWorkToggleButton(Dikuai dikuai) { JButton button = new JButton(); button.setContentAreaFilled(false); button.setBorder(BorderFactory.createEmptyBorder()); button.setFocusPainted(false); button.setOpaque(false); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.setPreferredSize(new Dimension(56, 56)); boolean isCurrent = dikuai.getLandNumber() != null && dikuai.getLandNumber().equals(currentWorkLandNumber); updateWorkToggleButton(button, isCurrent); button.addActionListener(e -> handleWorkToggle(dikuai)); return button; } private void updateWorkToggleButton(JButton button, boolean isCurrent) { ensureWorkIconsLoaded(); ImageIcon icon = isCurrent ? workSelectedIcon : workUnselectedIcon; if (icon != null) { button.setIcon(icon); button.setText(null); } else { button.setText(isCurrent ? "当前地块" : "设为当前"); } button.setToolTipText(isCurrent ? "取消当前作业地块" : "设为当前作业地块"); } private void ensureWorkIconsLoaded() { if (workSelectedIcon == null) { workSelectedIcon = loadIcon("image/open.png", 48, 48); } if (workUnselectedIcon == null) { workUnselectedIcon = loadIcon("image/close.png", 48, 48); } } private void handleWorkToggle(Dikuai dikuai) { String landNumber = dikuai.getLandNumber(); if (landNumber == null || landNumber.trim().isEmpty()) { return; } boolean isCurrent = landNumber.equals(currentWorkLandNumber); if (isCurrent) { setCurrentWorkLand(null, null); } else { setCurrentWorkLand(landNumber, dikuai.getLandName()); } loadDikuaiData(); } private void setCurrentWorklandIfNeeded(Dikuai dikuai) { if (dikuai == null) { return; } String landNumber = dikuai.getLandNumber(); if (landNumber == null || landNumber.trim().isEmpty()) { return; } setCurrentWorkLand(landNumber, dikuai.getLandName()); } public static void setCurrentWorkLand(String landNumber, String landName) { 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 (sanitizedLandNumber != null) { dikuai = Dikuai.getDikuai(sanitizedLandNumber); if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) { landName = dikuai.getLandName(); } } if (landName != null && ("-1".equals(landName) || landName.trim().isEmpty())) { landName = "未知地块"; } Shouye shouye = Shouye.getInstance(); if (shouye != 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; List 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(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); if (rawIcon.getIconWidth() <= 0 || rawIcon.getIconHeight() <= 0) { return null; } Image scaled = rawIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH); return new ImageIcon(scaled); } catch (Exception ex) { System.err.println("无法加载图标: " + path + " - " + ex.getMessage()); return null; } } private void setupEventHandlers() { // 新增地块按钮 addLandBtn.addActionListener(e -> { Window ownerWindow = SwingUtilities.getWindowAncestor(this); Window addDialogOwner = ownerWindow != null ? ownerWindow.getOwner() : null; Component parentComponent = addDialogOwner instanceof Component ? (Component) addDialogOwner : ownerWindow; if (ownerWindow instanceof JDialog) { ownerWindow.dispose(); } Coordinate.coordinates.clear(); latestInstance = null; SwingUtilities.invokeLater(() -> AddDikuai.showAddDikuaiDialog(parentComponent)); }); } private void editReturnPoint(Dikuai dikuai) { FanhuiDialog fd = new FanhuiDialog(SwingUtilities.getWindowAncestor(this), dikuai); fd.setVisible(true); // 如果对话框已更新数据,刷新显示 if (fd.isUpdated()) { loadDikuaiData(); } } private void addNewObstacle(Dikuai dikuai) { if (dikuai == null) { JOptionPane.showMessageDialog(this, "未找到当前地块,无法新增障碍物", "提示", JOptionPane.WARNING_MESSAGE); return; } Window windowAncestor = SwingUtilities.getWindowAncestor(this); if (windowAncestor instanceof JDialog) { windowAncestor.dispose(); } Component parent = windowAncestor != null ? windowAncestor.getOwner() instanceof Component ? (Component) windowAncestor.getOwner() : windowAncestor : null; List existingObstacleNames = loadObstacleNamesForLand(dikuai.getLandNumber()); addzhangaiwu.showDialog(parent, dikuai, existingObstacleNames); 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 loadObstacleSummaries() { Map 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 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 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 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 loadObstacleNamesForLand(String landNumber) { List names = new ArrayList<>(); if (landNumber == null || landNumber.trim().isEmpty()) { return names; } ObstacleSummary cached = getObstacleSummaryFromCache(landNumber); if (!cached.isEmpty()) { names.addAll(cached.copyNames()); return names; } Map latest = loadObstacleSummaries(); if (!latest.isEmpty()) { obstacleSummaryCache = latest; } ObstacleSummary refreshed = getObstacleSummaryFromCache(landNumber); if (!refreshed.isEmpty()) { names.addAll(refreshed.copyNames()); } return names; } private void editMowingWidth(Dikuai dikuai) { String currentWidth = dikuai.getMowingWidth(); if (currentWidth == null || "-1".equals(currentWidth)) { currentWidth = ""; } String input = JOptionPane.showInputDialog(this, "请输入割草宽度(单位: 厘米)", currentWidth); if (input == null) { return; } String trimmed = input.trim(); if (trimmed.isEmpty()) { JOptionPane.showMessageDialog(this, "割草宽度不能为空", "提示", JOptionPane.WARNING_MESSAGE); return; } String sanitized = trimmed; if (sanitized.endsWith("厘米")) { sanitized = sanitized.substring(0, sanitized.length() - 2).trim(); } if (sanitized.endsWith("cm") || sanitized.endsWith("CM")) { sanitized = sanitized.substring(0, sanitized.length() - 2).trim(); } double widthValue; try { widthValue = Double.parseDouble(sanitized); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, "请输入有效的数字", "提示", JOptionPane.WARNING_MESSAGE); return; } if (widthValue <= 0) { JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE); return; } String normalized = BigDecimal.valueOf(widthValue).stripTrailingZeros().toPlainString(); if (!Dikuai.updateField(dikuai.getLandNumber(), "mowingWidth", normalized)) { JOptionPane.showMessageDialog(this, "无法更新割草宽度", "错误", JOptionPane.ERROR_MESSAGE); return; } Dikuai.updateField(dikuai.getLandNumber(), "updateTime", getCurrentTime()); Dikuai.saveToProperties(); loadDikuaiData(); JOptionPane.showMessageDialog(this, "割草宽度已更新", "成功", JOptionPane.INFORMATION_MESSAGE); } private void deleteDikuai(Dikuai dikuai) { int result = JOptionPane.showConfirmDialog(this, "确定要删除地块 \"" + dikuai.getLandName() + "\" 吗?此操作无法撤销。", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (result == JOptionPane.YES_OPTION) { // 从内存中移除并同步到文件 if (Dikuai.removeDikuai(dikuai.getLandNumber())) { if (dikuai.getLandNumber() != null && dikuai.getLandNumber().equals(currentWorkLandNumber)) { setCurrentWorkLand(null, null); } boundaryPointVisibility.remove(dikuai.getLandNumber()); Dikuai.saveToProperties(); loadDikuaiData(); JOptionPane.showMessageDialog(this, "地块删除成功!", "成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, "未找到要删除的地块。", "提示", JOptionPane.WARNING_MESSAGE); } } } private String getCurrentTime() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(new Date()); } /** * 获取地块管理面板实例 * @param landNumber 地块编号 */ public static Dikuaiguanli createDikuaiManagementPanel(String landNumber) { // 确保地块数据已初始化 Dikuai.initFromProperties(); return new Dikuaiguanli(landNumber); } /** * 显示地块管理对话框(向后兼容) * @param parent 父组件 * @param landNumber 地块编号 */ public static void showDikuaiManagement(Component parent, String landNumber) { // 确保地块数据已初始化 Dikuai.initFromProperties(); JDialog dialog = new JDialog(); dialog.setTitle("地块管理"); dialog.setModal(true); dialog.setSize(400, 800); dialog.setLocationRelativeTo(parent); dialog.setResizable(false); Dikuaiguanli managementPanel = new Dikuaiguanli(landNumber); dialog.add(managementPanel); dialog.setVisible(true); } public static void notifyExternalCreation(String landNumber) { if (latestInstance == null) { return; } SwingUtilities.invokeLater(() -> latestInstance.loadDikuaiData()); } public static void updateBoundaryPointVisibility(String landNumber, boolean visible) { if (landNumber == null || landNumber.trim().isEmpty()) { return; } boundaryPointVisibility.put(landNumber, visible); } private static final class ObstacleSummary { private static final ObstacleSummary EMPTY = new ObstacleSummary(Collections.emptyList()); private final List names; private ObstacleSummary(List names) { this.names = names; } static ObstacleSummary of(List originalNames) { if (originalNames == null || originalNames.isEmpty()) { return empty(); } List 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 copyNames() { return new ArrayList<>(names); } } }