| | |
| | | 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.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 zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhuye.MapRenderer; |
| | |
| | | private JButton addLandBtn; |
| | | |
| | | private static String currentWorkLandNumber; |
| | | private static final String WORK_LAND_KEY = "currentWorkLandNumber"; |
| | | private static final String PROPERTIES_FILE = "set.properties"; |
| | | private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>(); |
| | | private ImageIcon workSelectedIcon; |
| | | private ImageIcon workUnselectedIcon; |
| | | private ImageIcon boundaryVisibleIcon; |
| | | private ImageIcon boundaryHiddenIcon; |
| | | private static final int BOUNDARY_TOGGLE_ICON_SIZE = 48; |
| | | private Map<String, ObstacleSummary> obstacleSummaryCache = Collections.emptyMap(); |
| | | |
| | | public Dikuaiguanli(String landNumber) { |
| | | latestInstance = this; |
| | |
| | | cardsPanel.removeAll(); |
| | | |
| | | Map<String, Dikuai> allDikuai = Dikuai.getAllDikuai(); |
| | | |
| | | |
| | | if (allDikuai.isEmpty()) { |
| | | obstacleSummaryCache = Collections.emptyMap(); |
| | | // 显示空状态 |
| | | JPanel emptyPanel = createEmptyStatePanel(); |
| | | cardsPanel.add(emptyPanel); |
| | | setCurrentWorkLand(null, null); |
| | | } else { |
| | | obstacleSummaryCache = loadObstacleSummaries(); |
| | | if (allDikuai.size() == 1) { |
| | | Dikuai onlyDikuai = allDikuai.values().iterator().next(); |
| | | setCurrentWorklandIfNeeded(onlyDikuai); |
| | |
| | | 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))); |
| | | |
| | | // 障碍物坐标(带新增和查看按钮) |
| | | JPanel obstaclePanel = createCardInfoItemWithButton("障碍物坐标:", |
| | | getTruncatedValue(dikuai.getObstacleCoordinates(), 12, "未设置"), |
| | | ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber()); |
| | | JPanel obstaclePanel = createCardInfoItemWithButton("障碍物:", |
| | | obstacleSummary.buildDisplayValue(), |
| | | "新增", |
| | | e -> addNewObstacle(dikuai)); |
| | | setInfoItemTooltip(obstaclePanel, dikuai.getObstacleCoordinates()); |
| | | setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip()); |
| | | contentPanel.add(obstaclePanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | |
| | | 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))); |
| | | |
| | |
| | | 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))); |
| | | |
| | |
| | | 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))); |
| | | |
| | |
| | | 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))); |
| | | |
| | |
| | | "编辑", 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); |
| | | |
| | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | |
| | | boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber); |
| | | if (isCurrent) { |
| | | renderer.setBoundaryPointsVisible(desiredState); |
| | | renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d); |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | }); |
| | | |
| | |
| | | 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); |
| | |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | |
| | | |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | if (landNumber == null) { |
| | | if (sanitizedLandNumber == null) { |
| | | shouye.updateCurrentAreaName(null); |
| | | } else { |
| | | shouye.updateCurrentAreaName(landName); |
| | | } |
| | | MapRenderer renderer = shouye.getMapRenderer(); |
| | | if (renderer != null) { |
| | | renderer.applyLandMetadata(dikuai); |
| | | String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null; |
| | | String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null; |
| | | String obstacles = (dikuai != null) ? dikuai.getObstacleCoordinates() : null; |
| | | renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName); |
| | | List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList(); |
| | | if (configuredObstacles == null) { |
| | | configuredObstacles = Collections.emptyList(); |
| | | } |
| | | renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName); |
| | | renderer.setCurrentPlannedPath(plannedPath); |
| | | renderer.setCurrentObstacles(obstacles, landNumber); |
| | | boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false); |
| | | renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber); |
| | | boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false); |
| | | renderer.setBoundaryPointsVisible(showBoundaryPoints); |
| | | renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d); |
| | | } |
| | | shouye.refreshMowingIndicators(); |
| | | } |
| | | |
| | | if (changed) { |
| | | persistCurrentWorkLand(sanitizedLandNumber); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | loadDikuaiData(); |
| | | } |
| | | |
| | | private void showCompletedMowingTrackDialog(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | JDialog dialog = new JDialog(owner, "已完成的割草路径坐标", Dialog.ModalityType.APPLICATION_MODAL); |
| | | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | dialog.setLayout(new BorderLayout()); |
| | | dialog.getContentPane().setBackground(WHITE); |
| | | |
| | | String normalizedTrack = prepareCoordinateForEditor(dikuai.getMowingTrack()); |
| | | boolean hasTrack = normalizedTrack != null && !normalizedTrack.isEmpty(); |
| | | |
| | | JTextArea textArea = new JTextArea(hasTrack ? normalizedTrack : ""); |
| | | textArea.setEditable(false); |
| | | textArea.setLineWrap(true); |
| | | textArea.setWrapStyleWord(true); |
| | | textArea.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); |
| | | textArea.setCaretPosition(0); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setPreferredSize(new Dimension(342, 240)); |
| | | scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12)); |
| | | |
| | | JLabel statusLabel = new JLabel(hasTrack ? "当前已保存完成的割草路径记录。" : "当前暂无完成的割草路径记录。"); |
| | | statusLabel.setBorder(BorderFactory.createEmptyBorder(12, 12, 6, 12)); |
| | | statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | statusLabel.setForeground(hasTrack ? TEXT_COLOR : LIGHT_TEXT); |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); |
| | | buttonPanel.setBackground(WHITE); |
| | | buttonPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); |
| | | |
| | | JButton deleteButton = createSmallButton("删除记录", RED_COLOR, RED_DARK); |
| | | deleteButton.setEnabled(hasTrack); |
| | | JButton closeButton = createSmallButton("关闭", PRIMARY_COLOR, PRIMARY_DARK); |
| | | |
| | | deleteButton.addActionListener(e -> { |
| | | String latestTrack = prepareCoordinateForEditor(dikuai.getMowingTrack()); |
| | | if (latestTrack == null || latestTrack.isEmpty()) { |
| | | JOptionPane.showMessageDialog(dialog, "当前没有可删除的轨迹记录。", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | int choice = JOptionPane.showConfirmDialog(dialog, "确定要删除已完成的割草路径记录吗?", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); |
| | | if (choice != JOptionPane.YES_OPTION) { |
| | | return; |
| | | } |
| | | if (saveFieldAndRefresh(dikuai, "mowingTrack", "-1")) { |
| | | dikuai.setMowingTrack("-1"); |
| | | textArea.setText(""); |
| | | statusLabel.setText("当前暂无完成的割草路径记录。"); |
| | | statusLabel.setForeground(LIGHT_TEXT); |
| | | deleteButton.setEnabled(false); |
| | | JOptionPane.showMessageDialog(dialog, "已删除完成路径记录", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } else { |
| | | JOptionPane.showMessageDialog(dialog, "删除失败,请稍后重试。", "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | }); |
| | | |
| | | closeButton.addActionListener(e -> dialog.dispose()); |
| | | |
| | | buttonPanel.add(deleteButton); |
| | | buttonPanel.add(closeButton); |
| | | |
| | | dialog.add(statusLabel, BorderLayout.NORTH); |
| | | dialog.add(scrollPane, BorderLayout.CENTER); |
| | | dialog.add(buttonPanel, BorderLayout.SOUTH); |
| | | dialog.pack(); |
| | | dialog.setMinimumSize(new Dimension(378, 320)); |
| | | dialog.setLocationRelativeTo(owner); |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | private Map<String, ObstacleSummary> loadObstacleSummaries() { |
| | | Map<String, ObstacleSummary> summaries = new HashMap<>(); |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return summaries; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return summaries; |
| | | } |
| | | for (Obstacledge.Plot plot : manager.getPlots()) { |
| | | if (plot == null) { |
| | | continue; |
| | | } |
| | | String plotId = plot.getPlotId(); |
| | | if (plotId == null || plotId.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | List<String> names = new ArrayList<>(); |
| | | for (Obstacledge.Obstacle obstacle : plot.getObstacles()) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String name = obstacle.getObstacleName(); |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (!trimmed.isEmpty()) { |
| | | names.add(trimmed); |
| | | } |
| | | } |
| | | summaries.put(plotId.trim(), ObstacleSummary.of(names)); |
| | | } |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | } |
| | | return summaries; |
| | | } |
| | | |
| | | private static List<Obstacledge.Obstacle> loadObstaclesFromConfig(String landNumber) { |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return null; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return null; |
| | | } |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); |
| | | if (plot == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Obstacledge.Obstacle> obstacles = plot.getObstacles(); |
| | | if (obstacles == null || obstacles.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return new ArrayList<>(obstacles); |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private ObstacleSummary getObstacleSummaryFromCache(String landNumber) { |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return ObstacleSummary.empty(); |
| | | } |
| | | if (obstacleSummaryCache == null || obstacleSummaryCache.isEmpty()) { |
| | | return ObstacleSummary.empty(); |
| | | } |
| | | ObstacleSummary summary = obstacleSummaryCache.get(landNumber.trim()); |
| | | return summary != null ? summary : ObstacleSummary.empty(); |
| | | } |
| | | |
| | | private List<String> loadObstacleNamesForLand(String landNumber) { |
| | | List<String> names = new ArrayList<>(); |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return names; |
| | | } |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return names; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return names; |
| | | } |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); |
| | | if (plot == null) { |
| | | return names; |
| | | } |
| | | for (Obstacledge.Obstacle obstacle : plot.getObstacles()) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String name = obstacle.getObstacleName(); |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (!trimmed.isEmpty() && !names.contains(trimmed)) { |
| | | names.add(trimmed); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | ObstacleSummary cached = getObstacleSummaryFromCache(landNumber); |
| | | if (!cached.isEmpty()) { |
| | | names.addAll(cached.copyNames()); |
| | | return names; |
| | | } |
| | | Map<String, ObstacleSummary> latest = loadObstacleSummaries(); |
| | | if (!latest.isEmpty()) { |
| | | obstacleSummaryCache = latest; |
| | | } |
| | | ObstacleSummary refreshed = getObstacleSummaryFromCache(landNumber); |
| | | if (!refreshed.isEmpty()) { |
| | | names.addAll(refreshed.copyNames()); |
| | | } |
| | | return names; |
| | | } |
| | |
| | | } |
| | | boundaryPointVisibility.put(landNumber, visible); |
| | | } |
| | | |
| | | private static final class ObstacleSummary { |
| | | private static final ObstacleSummary EMPTY = new ObstacleSummary(Collections.emptyList()); |
| | | private final List<String> names; |
| | | |
| | | private ObstacleSummary(List<String> names) { |
| | | this.names = names; |
| | | } |
| | | |
| | | static ObstacleSummary of(List<String> originalNames) { |
| | | if (originalNames == null || originalNames.isEmpty()) { |
| | | return empty(); |
| | | } |
| | | List<String> cleaned = new ArrayList<>(); |
| | | for (String name : originalNames) { |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | continue; |
| | | } |
| | | boolean duplicated = false; |
| | | for (String existing : cleaned) { |
| | | if (existing.equalsIgnoreCase(trimmed)) { |
| | | duplicated = true; |
| | | break; |
| | | } |
| | | } |
| | | if (!duplicated) { |
| | | cleaned.add(trimmed); |
| | | } |
| | | } |
| | | if (cleaned.isEmpty()) { |
| | | return empty(); |
| | | } |
| | | cleaned.sort(String::compareToIgnoreCase); |
| | | return new ObstacleSummary(Collections.unmodifiableList(cleaned)); |
| | | } |
| | | |
| | | static ObstacleSummary empty() { |
| | | return EMPTY; |
| | | } |
| | | |
| | | boolean isEmpty() { |
| | | return names.isEmpty(); |
| | | } |
| | | |
| | | int count() { |
| | | return names.size(); |
| | | } |
| | | |
| | | String buildDisplayValue() { |
| | | return count() > 0 ? String.format("障碍物%d个", count()) : "暂无障碍物"; |
| | | } |
| | | |
| | | String buildTooltip() { |
| | | return count() > 0 ? String.join(",", names) : "暂无障碍物"; |
| | | } |
| | | |
| | | List<String> copyNames() { |
| | | return new ArrayList<>(names); |
| | | } |
| | | } |
| | | } |