| | |
| | | import java.awt.Font; |
| | | import java.awt.Image; |
| | | import java.awt.Window; |
| | | import java.awt.Container; |
| | | import java.awt.event.ActionListener; |
| | | import java.awt.event.MouseAdapter; |
| | | import java.awt.event.MouseEvent; |
| | | import java.awt.event.MouseListener; |
| | | import java.awt.event.WindowAdapter; |
| | | import java.awt.event.WindowEvent; |
| | | import java.io.File; |
| | |
| | | import javax.swing.JPanel; |
| | | import javax.swing.SwingConstants; |
| | | import javax.swing.SwingUtilities; |
| | | import javax.swing.JTextField; |
| | | import javax.swing.border.Border; |
| | | import javax.swing.event.DocumentEvent; |
| | | import javax.swing.event.DocumentListener; |
| | | |
| | | import baseStation.BaseStation; |
| | | import gecaoji.Device; |
| | | import set.Setsys; |
| | | import ui.UIConfig; |
| | | import zhuye.Coordinate; |
| | | import zhuye.MapRenderer; |
| | | import zhuye.Shouye; |
| | | import zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhangaiwu.yulanzhangaiwu; |
| | | import zhuye.buttonset; |
| | | import bianjie.bianjieguihua2; |
| | | import bianjie.ThreePointCircle; |
| | | |
| | | /** |
| | | * 障碍物新增/编辑对话框。设计语言参考 {@link AddDikuai},支持通过实地绘制采集障碍物坐标。 |
| | |
| | | private final Color MEDIUM_GRAY = new Color(233, 236, 239); |
| | | private final Color TEXT_COLOR = new Color(33, 37, 41); |
| | | private final Color LIGHT_TEXT = new Color(108, 117, 125); |
| | | private final Color DANGER_COLOR = new Color(220, 53, 69); |
| | | private final Color DANGER_LIGHT = new Color(255, 235, 238); |
| | | |
| | | private final Dikuai targetDikuai; |
| | | private final List<ExistingObstacle> existingObstacles; |
| | |
| | | private JPanel selectedMethodPanel; |
| | | private JPanel selectedShapePanel; |
| | | private JButton drawButton; |
| | | private JButton generateBoundaryButton; // 生成障碍物边界按钮 |
| | | private JButton previewButton; // 预览按钮 |
| | | private JLabel drawingStatusLabel; |
| | | private JLabel boundaryStatusLabel; // 生成障碍物边界状态标签 |
| | | private JTextField obstacleNameField; |
| | | private JPanel existingObstacleListPanel; |
| | | private JPanel step1NextButtonRow; |
| | | |
| | | private int currentStep = 1; |
| | | private boolean drawingInProgress; |
| | |
| | | if (target == null) { |
| | | throw new IllegalArgumentException("targetDikuai 不能为空"); |
| | | } |
| | | this.targetDikuai = target; |
| | | this.existingObstacles = Collections.unmodifiableList(resolveExistingObstacles(target, obstacleNames)); |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.setStartSaveGngga(false); |
| | | Coordinate.clearActiveDeviceIdFilter(); |
| | | this.targetDikuai = target; |
| | | this.existingObstacles = new ArrayList<>(resolveExistingObstacles(target, obstacleNames)); |
| | | initializeUI(); |
| | | setupEventHandlers(); |
| | | preloadData(); |
| | |
| | | infoContainer.add(createInfoRow("地块编号:", safeValue(targetDikuai.getLandNumber(), "未设置"))); |
| | | infoContainer.add(Box.createRigidArea(new Dimension(0, 16))); |
| | | infoContainer.add(createInfoRow("地块名称:", safeValue(targetDikuai.getLandName(), "未命名"))); |
| | | infoContainer.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | infoContainer.add(Box.createRigidArea(new Dimension(0, 16))); |
| | | infoContainer.add(createObstacleNameRow()); |
| | | infoContainer.add(Box.createRigidArea(new Dimension(0, 12))); |
| | | infoContainer.add(createStep1NextButtonRow()); |
| | | infoContainer.add(Box.createRigidArea(new Dimension(0, 12))); |
| | | infoContainer.add(buildExistingObstacleSection()); |
| | | |
| | | stepPanel.add(infoContainer); |
| | |
| | | section.add(titleLabel); |
| | | section.add(Box.createRigidArea(new Dimension(0, 8))); |
| | | |
| | | existingObstacleListPanel = new JPanel(); |
| | | existingObstacleListPanel.setLayout(new BoxLayout(existingObstacleListPanel, BoxLayout.Y_AXIS)); |
| | | existingObstacleListPanel.setOpaque(false); |
| | | existingObstacleListPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | section.add(existingObstacleListPanel); |
| | | refreshExistingObstacleList(); |
| | | return section; |
| | | } |
| | | |
| | | private JPanel createStep1NextButtonRow() { |
| | | if (step1NextButtonRow == null) { |
| | | step1NextButtonRow = new JPanel(); |
| | | step1NextButtonRow.setLayout(new BoxLayout(step1NextButtonRow, BoxLayout.X_AXIS)); |
| | | step1NextButtonRow.setOpaque(false); |
| | | step1NextButtonRow.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | step1NextButtonRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36)); |
| | | step1NextButtonRow.add(Box.createHorizontalGlue()); |
| | | } |
| | | return step1NextButtonRow; |
| | | } |
| | | |
| | | private void attachNextButtonToStep1Row() { |
| | | if (step1NextButtonRow == null || nextButton == null) { |
| | | return; |
| | | } |
| | | step1NextButtonRow.removeAll(); |
| | | step1NextButtonRow.add(Box.createHorizontalGlue()); |
| | | nextButton.setAlignmentX(Component.RIGHT_ALIGNMENT); |
| | | step1NextButtonRow.add(nextButton); |
| | | step1NextButtonRow.revalidate(); |
| | | step1NextButtonRow.repaint(); |
| | | } |
| | | |
| | | private void refreshExistingObstacleList() { |
| | | if (existingObstacleListPanel == null) { |
| | | return; |
| | | } |
| | | existingObstacleListPanel.removeAll(); |
| | | if (existingObstacles.isEmpty()) { |
| | | JLabel emptyLabel = new JLabel("暂无"); |
| | | emptyLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | emptyLabel.setForeground(LIGHT_TEXT); |
| | | emptyLabel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | section.add(emptyLabel); |
| | | return section; |
| | | } |
| | | |
| | | JPanel listPanel = new JPanel(); |
| | | listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.Y_AXIS)); |
| | | listPanel.setOpaque(false); |
| | | listPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | for (int i = 0; i < existingObstacles.size(); i++) { |
| | | ExistingObstacle obstacle = existingObstacles.get(i); |
| | | JPanel row = createObstacleSummaryRow(obstacle); |
| | | row.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | listPanel.add(row); |
| | | if (i < existingObstacles.size() - 1) { |
| | | listPanel.add(Box.createRigidArea(new Dimension(0, 6))); |
| | | existingObstacleListPanel.add(emptyLabel); |
| | | } else { |
| | | for (int i = 0; i < existingObstacles.size(); i++) { |
| | | ExistingObstacle obstacle = existingObstacles.get(i); |
| | | JPanel row = createObstacleSummaryRow(obstacle); |
| | | row.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | existingObstacleListPanel.add(row); |
| | | if (i < existingObstacles.size() - 1) { |
| | | existingObstacleListPanel.add(Box.createRigidArea(new Dimension(0, 6))); |
| | | } |
| | | } |
| | | } |
| | | |
| | | section.add(listPanel); |
| | | return section; |
| | | existingObstacleListPanel.revalidate(); |
| | | existingObstacleListPanel.repaint(); |
| | | } |
| | | |
| | | private JPanel createObstacleSummaryRow(ExistingObstacle obstacle) { |
| | |
| | | JButton editButton = createInlineButton("修改"); |
| | | editButton.addActionListener(e -> populateObstacleForEditing(obstacle)); |
| | | |
| | | JButton deleteButton = createInlineIconButton("image/delete.png", "删除障碍物"); |
| | | deleteButton.addActionListener(e -> handleDeleteExistingObstacle(obstacle)); |
| | | |
| | | row.add(infoLabel); |
| | | row.add(Box.createHorizontalGlue()); |
| | | row.add(editButton); |
| | | row.add(Box.createRigidArea(new Dimension(6, 0))); |
| | | row.add(deleteButton); |
| | | return row; |
| | | } |
| | | |
| | | private JButton createInlineButton(String text) { |
| | | JButton button = new JButton(text); |
| | | JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR); |
| | | button.setFont(new Font("微软雅黑", Font.BOLD, 12)); |
| | | button.setForeground(WHITE); |
| | | button.setBackground(PRIMARY_COLOR); |
| | | button.setBorder(BorderFactory.createEmptyBorder(6, 16, 6, 16)); |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.setFocusPainted(false); |
| | | Dimension size = new Dimension(72, 28); |
| | | button.setPreferredSize(size); |
| | | button.setMinimumSize(size); |
| | |
| | | return button; |
| | | } |
| | | |
| | | private JButton createInlineIconButton(String iconPath, String tooltip) { |
| | | JButton button = new JButton(); |
| | | button.setPreferredSize(new Dimension(32, 28)); |
| | | button.setMinimumSize(new Dimension(32, 28)); |
| | | button.setMaximumSize(new Dimension(32, 28)); |
| | | button.setBackground(WHITE); |
| | | button.setBorder(BorderFactory.createLineBorder(DANGER_COLOR)); |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.setFocusPainted(false); |
| | | button.setFocusable(false); |
| | | button.setOpaque(true); |
| | | button.setContentAreaFilled(true); |
| | | if (tooltip != null && !tooltip.trim().isEmpty()) { |
| | | button.setToolTipText(tooltip); |
| | | } |
| | | |
| | | ImageIcon icon = null; |
| | | if (iconPath != null && !iconPath.trim().isEmpty()) { |
| | | ImageIcon rawIcon = new ImageIcon(iconPath); |
| | | if (rawIcon.getIconWidth() > 0 && rawIcon.getIconHeight() > 0) { |
| | | Image scaled = rawIcon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH); |
| | | icon = new ImageIcon(scaled); |
| | | } |
| | | } |
| | | if (icon != null) { |
| | | button.setIcon(icon); |
| | | } else { |
| | | button.setText("删"); |
| | | button.setFont(new Font("微软雅黑", Font.BOLD, 12)); |
| | | button.setForeground(DANGER_COLOR); |
| | | } |
| | | |
| | | button.addMouseListener(new MouseAdapter() { |
| | | @Override |
| | | public void mouseEntered(MouseEvent e) { |
| | | button.setBackground(DANGER_LIGHT); |
| | | } |
| | | |
| | | @Override |
| | | public void mouseExited(MouseEvent e) { |
| | | button.setBackground(WHITE); |
| | | } |
| | | }); |
| | | return button; |
| | | } |
| | | |
| | | private String buildObstacleSummaryText(ExistingObstacle obstacle) { |
| | | String name = obstacle.getName(); |
| | | String shape = obstacle.getShapeDisplay(); |
| | | String coordPreview = buildCoordinatePreview(obstacle.getCoordinates(), 5); |
| | | String coordPreview = buildCoordinatePreview(obstacle.getDisplayCoordinates(), 5); |
| | | return String.format(Locale.CHINA, "%s,%s,坐标:%s", name, shape, coordPreview); |
| | | } |
| | | |
| | |
| | | return sanitized.substring(0, keepLength) + "..."; |
| | | } |
| | | |
| | | private boolean deleteObstacleFromConfig(String obstacleName) { |
| | | String landNumber = targetDikuai != null ? targetDikuai.getLandNumber() : null; |
| | | if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName)) { |
| | | return false; |
| | | } |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return false; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return false; |
| | | } |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); |
| | | if (plot == null) { |
| | | return false; |
| | | } |
| | | if (!plot.removeObstacleByName(obstacleName.trim())) { |
| | | return false; |
| | | } |
| | | if (!manager.saveToFile(configFile.getAbsolutePath())) { |
| | | return false; |
| | | } |
| | | Dikuai.updateField(landNumber.trim(), "updateTime", currentTime()); |
| | | Dikuai.saveToProperties(); |
| | | Dikuaiguanli.notifyExternalCreation(landNumber.trim()); |
| | | return true; |
| | | } catch (Exception ex) { |
| | | System.err.println("删除障碍物失败: " + ex.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void populateObstacleForEditing(ExistingObstacle obstacle) { |
| | | if (obstacle == null) { |
| | | return; |
| | | } |
| | | String coords = obstacle.getCoordinates(); |
| | | if (isMeaningfulValue(coords)) { |
| | | formData.put("obstacleCoordinates", coords.trim()); |
| | | String name = obstacle.getName(); |
| | | if (isMeaningfulValue(name)) { |
| | | formData.put("obstacleName", name.trim()); |
| | | formData.put("editingObstacleName", name.trim()); |
| | | } else { |
| | | formData.remove("obstacleName"); |
| | | formData.remove("editingObstacleName"); |
| | | } |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.setText(isMeaningfulValue(name) ? name.trim() : ""); |
| | | } |
| | | String xyCoords = obstacle.getXyCoordinates(); |
| | | String displayCoords = obstacle.getDisplayCoordinates(); |
| | | if (isMeaningfulValue(xyCoords)) { |
| | | formData.put("obstacleCoordinates", xyCoords.trim()); |
| | | } else if (isMeaningfulValue(displayCoords)) { |
| | | formData.put("obstacleCoordinates", displayCoords.trim()); |
| | | } else { |
| | | formData.remove("obstacleCoordinates"); |
| | | } |
| | | String originalCoords = obstacle.getOriginalCoordinates(); |
| | | if (isMeaningfulValue(originalCoords)) { |
| | | formData.put("obstacleOriginalCoordinates", originalCoords.trim()); |
| | | } else { |
| | | formData.remove("obstacleOriginalCoordinates"); |
| | | } |
| | | String shapeKey = obstacle.getShapeKey(); |
| | | if (shapeKey != null) { |
| | | JPanel shapePanel = shapeOptionPanels.get(shapeKey); |
| | |
| | | showStep(2); |
| | | } |
| | | |
| | | private void handleDeleteExistingObstacle(ExistingObstacle obstacle) { |
| | | if (obstacle == null) { |
| | | return; |
| | | } |
| | | String obstacleName = obstacle.getName(); |
| | | if (!isMeaningfulValue(obstacleName)) { |
| | | JOptionPane.showMessageDialog(this, "无法删除:障碍物名称无效", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | int choice = JOptionPane.showConfirmDialog(this, |
| | | "确定要删除障碍物 \"" + obstacleName + "\" 吗?此操作无法撤销。", |
| | | "删除确认", |
| | | JOptionPane.YES_NO_OPTION, |
| | | JOptionPane.WARNING_MESSAGE); |
| | | if (choice != JOptionPane.YES_OPTION) { |
| | | return; |
| | | } |
| | | boolean removedFromConfig = deleteObstacleFromConfig(obstacleName); |
| | | boolean hasPersistedData = obstacle.hasPersistedData() || obstacle.getShapeKey() != null; |
| | | if (!removedFromConfig && hasPersistedData) { |
| | | JOptionPane.showMessageDialog(this, "删除失败,未能在配置中移除该障碍物。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | if (!existingObstacles.remove(obstacle)) { |
| | | existingObstacles.removeIf(item -> obstacleName.equals(item != null ? item.getName() : null)); |
| | | } |
| | | refreshExistingObstacleList(); |
| | | clearEditingStateIfDeleted(obstacleName); |
| | | JOptionPane.showMessageDialog(this, "障碍物已删除", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void clearEditingStateIfDeleted(String obstacleName) { |
| | | if (!isMeaningfulValue(obstacleName)) { |
| | | return; |
| | | } |
| | | String editingName = formData.get("editingObstacleName"); |
| | | if (isMeaningfulValue(editingName) && obstacleName.equals(editingName)) { |
| | | formData.remove("editingObstacleName"); |
| | | formData.remove("obstacleCoordinates"); |
| | | formData.remove("obstacleOriginalCoordinates"); |
| | | formData.remove("generatedBoundaryCoordinates"); // 清除生成的边界坐标 |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.setText(""); |
| | | } else { |
| | | formData.remove("obstacleName"); |
| | | } |
| | | updateDrawingStatus(); |
| | | updateSaveButtonState(); |
| | | updatePreviewButtonState(); |
| | | } |
| | | } |
| | | |
| | | private JPanel createStep2Panel() { |
| | | JPanel stepPanel = new JPanel(); |
| | | stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS)); |
| | |
| | | stepPanel.add(methodOptionsPanel); |
| | | |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | stepPanel.add(createSectionHeader("障碍物形状", "多边形需采集多个点,圆形只需圆心与圆周上一点")); |
| | | stepPanel.add(createSectionHeader("障碍物形状", "多边形需采集多个点,圆形需在圆周上采集至少三个点")); |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | shapeOptionsPanel = new JPanel(); |
| | |
| | | shapeOptionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | shapeOptionsPanel.add(createShapeOption("polygon", "多边形", "依次采集轮廓上的多个点或者沿障碍物边缘走一圈")); |
| | | shapeOptionsPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | shapeOptionsPanel.add(createShapeOption("circle", "圆形", "先采集圆心坐标,再采集圆周上一点")); |
| | | shapeOptionsPanel.add(createShapeOption("circle", "圆形", "沿圆周任意采集三个点自动生成圆")); |
| | | stepPanel.add(shapeOptionsPanel); |
| | | |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 24))); |
| | | |
| | | // 按钮容器:重新绘制 + 生成障碍物边界 |
| | | JPanel buttonRow = new JPanel(); |
| | | buttonRow.setLayout(new BoxLayout(buttonRow, BoxLayout.X_AXIS)); |
| | | buttonRow.setOpaque(false); |
| | | buttonRow.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | drawButton = createPrimaryButton("开始绘制", 16); |
| | | drawButton.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | drawButton.addActionListener(e -> startDrawingWorkflow()); |
| | | stepPanel.add(drawButton); |
| | | buttonRow.add(drawButton); |
| | | |
| | | buttonRow.add(Box.createRigidArea(new Dimension(10, 0))); |
| | | |
| | | generateBoundaryButton = createPrimaryButton("生成障碍物边界", 16); |
| | | generateBoundaryButton.setVisible(false); // 初始隐藏,绘制完成后显示 |
| | | generateBoundaryButton.addActionListener(e -> generateObstacleBoundary()); |
| | | buttonRow.add(generateBoundaryButton); |
| | | |
| | | stepPanel.add(buttonRow); |
| | | |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 12))); |
| | | |
| | |
| | | drawingStatusLabel.setForeground(LIGHT_TEXT); |
| | | drawingStatusLabel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | stepPanel.add(drawingStatusLabel); |
| | | |
| | | // 添加生成障碍物边界状态标签 |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 8))); |
| | | boundaryStatusLabel = new JLabel(""); |
| | | boundaryStatusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | boundaryStatusLabel.setForeground(LIGHT_TEXT); |
| | | boundaryStatusLabel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | boundaryStatusLabel.setVisible(false); // 初始隐藏 |
| | | stepPanel.add(boundaryStatusLabel); |
| | | |
| | | stepPanel.add(Box.createVerticalGlue()); |
| | | return stepPanel; |
| | |
| | | |
| | | prevButton = createSecondaryButton("上一步"); |
| | | nextButton = createPrimaryButton("下一步", 16); |
| | | // 设置下一步按钮宽度为300像素 |
| | | nextButton.setPreferredSize(new Dimension(300, nextButton.getPreferredSize().height)); |
| | | nextButton.setMaximumSize(new Dimension(300, nextButton.getPreferredSize().height)); |
| | | |
| | | previewButton = createSecondaryButton("预览"); |
| | | previewButton.setVisible(false); // 初始隐藏,生成边界后显示 |
| | | previewButton.setOpaque(true); |
| | | previewButton.setContentAreaFilled(true); |
| | | previewButton.addActionListener(e -> previewObstacleBoundary()); |
| | | |
| | | saveButton = createPrimaryButton("保存", 16); |
| | | saveButton.setVisible(false); |
| | | |
| | | buttonPanel.add(prevButton); |
| | | buttonPanel.add(Box.createHorizontalGlue()); |
| | | buttonPanel.add(nextButton); |
| | | buttonPanel.add(Box.createRigidArea(new Dimension(12, 0))); |
| | | buttonPanel.add(previewButton); |
| | | buttonPanel.add(Box.createRigidArea(new Dimension(10, 0))); |
| | | buttonPanel.add(saveButton); |
| | | |
| | | attachNextButtonToStep1Row(); |
| | | |
| | | return buttonPanel; |
| | | } |
| | | |
| | |
| | | if (option == null) { |
| | | return; |
| | | } |
| | | if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) { |
| | | JOptionPane.showMessageDialog(this, "请先去系统设置添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | if (selectedMethodPanel != null && selectedMethodPanel != option) { |
| | | resetOptionAppearance(selectedMethodPanel); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private boolean hasConfiguredHandheldMarker() { |
| | | String handheldId = Setsys.getPropertyValue("handheldMarkerId"); |
| | | return handheldId != null && !handheldId.trim().isEmpty(); |
| | | } |
| | | |
| | | private void selectShapeOption(JPanel option, String type, boolean userTriggered) { |
| | | if (option == null) { |
| | | return; |
| | |
| | | |
| | | nextButton.addActionListener(e -> { |
| | | if (currentStep == 1) { |
| | | String name = obstacleNameField != null ? obstacleNameField.getText() : null; |
| | | if (!isMeaningfulValue(name)) { |
| | | JOptionPane.showMessageDialog(this, "请先填写障碍物名称", "提示", JOptionPane.WARNING_MESSAGE); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.requestFocusInWindow(); |
| | | } |
| | | return; |
| | | } |
| | | if (!isObstacleNameUnique(name)) { |
| | | JOptionPane.showMessageDialog(this, "障碍物名称已存在,请输入唯一名称", "提示", JOptionPane.WARNING_MESSAGE); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.requestFocusInWindow(); |
| | | } |
| | | return; |
| | | } |
| | | formData.put("obstacleName", name.trim()); |
| | | Coordinate.coordinates.clear(); |
| | | showStep(2); |
| | | } |
| | |
| | | } |
| | | |
| | | activeDrawingShape = null; |
| | | |
| | | // 重新绘制时清除之前生成的边界坐标 |
| | | formData.remove("generatedBoundaryCoordinates"); |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(false); |
| | | } |
| | | // 清除边界状态标签 |
| | | if (boundaryStatusLabel != null) { |
| | | boundaryStatusLabel.setVisible(false); |
| | | boundaryStatusLabel.setText(""); |
| | | } |
| | | // 对于圆形障碍物,隐藏生成边界按钮 |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | boolean isCircle = "circle".equals(shapeKey); |
| | | if (isCircle && generateBoundaryButton != null) { |
| | | generateBoundaryButton.setVisible(false); |
| | | } |
| | | |
| | | String method = formData.get("drawingMethod"); |
| | | if (!isMeaningfulValue(method)) { |
| | |
| | | return; |
| | | } |
| | | |
| | | String deviceId = resolveDrawingDeviceId(method); |
| | | if (!isMeaningfulValue(deviceId)) { |
| | | String idLabel = "handheld".equalsIgnoreCase(method) ? "手持设备编号" : "割草机编号"; |
| | | JOptionPane.showMessageDialog(this, |
| | | "未获取到有效的" + idLabel + ",请先在系统设置中完成配置", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | activeDrawingShape = shape.toLowerCase(Locale.ROOT); |
| | | |
| | | Shouye shouyeInstance = Shouye.getInstance(); |
| | | if (shouyeInstance != null) { |
| | | shouyeInstance.setHandheldMowerIconActive("handheld".equalsIgnoreCase(method)); |
| | | MapRenderer renderer = shouyeInstance.getMapRenderer(); |
| | | if (renderer != null) { |
| | | renderer.clearIdleTrail(); |
| | | } |
| | | } |
| | | |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.setActiveDeviceIdFilter(deviceId); |
| | | Coordinate.setStartSaveGngga(true); |
| | | yulanzhangaiwu.startPreview(activeDrawingShape, baseStation); |
| | | drawingInProgress = true; |
| | | drawButton.setText("正在绘制..."); |
| | | drawButton.setEnabled(false); |
| | | drawingStatusLabel.setText("正在采集障碍物坐标,请在主界面完成绘制。"); |
| | | if ("circle".equals(activeDrawingShape)) { |
| | | drawingStatusLabel.setText("正在采集圆形障碍物,请沿圆周采集至少三个点后在主界面结束绘制。"); |
| | | } else { |
| | | drawingStatusLabel.setText("正在采集障碍物坐标,请在主界面完成绘制。"); |
| | | } |
| | | |
| | | if (activeSession == null) { |
| | | activeSession = new ObstacleDrawingSession(); |
| | |
| | | |
| | | public static void finishDrawingSession() { |
| | | Coordinate.setStartSaveGngga(false); |
| | | Coordinate.clearActiveDeviceIdFilter(); |
| | | yulanzhangaiwu.stopPreview(); |
| | | |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | |
| | | showDialog(parent, activeSession.target); |
| | | } |
| | | |
| | | public static String getActiveSessionBaseStation() { |
| | | if (activeSession != null && isMeaningfulValue(activeSession.baseStation)) { |
| | | return activeSession.baseStation.trim(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Stops an in-progress circle capture and reopens the wizard on step 2. |
| | | */ |
| | | public static void abortCircleDrawingAndReturn(String message) { |
| | | Coordinate.setStartSaveGngga(false); |
| | | Coordinate.clearActiveDeviceIdFilter(); |
| | | yulanzhangaiwu.stopPreview(); |
| | | Coordinate.coordinates.clear(); |
| | | |
| | | if (activeSession == null || activeSession.target == null) { |
| | |
| | | return; |
| | | } |
| | | |
| | | String originalCoordStr = buildOriginalCoordinateString(captured); |
| | | if (isMeaningfulValue(originalCoordStr)) { |
| | | session.data.put("obstacleOriginalCoordinates", originalCoordStr); |
| | | } else { |
| | | session.data.remove("obstacleOriginalCoordinates"); |
| | | } |
| | | |
| | | List<double[]> xyPoints = convertToLocalXY(captured, session.baseStation); |
| | | if (xyPoints.isEmpty()) { |
| | | session.captureSuccessful = false; |
| | |
| | | |
| | | String shape = session.data.get("obstacleShape"); |
| | | if ("circle".equals(shape)) { |
| | | if (xyPoints.size() < 2) { |
| | | if (xyPoints.size() < 3) { |
| | | session.captureSuccessful = false; |
| | | session.captureMessage = "圆形障碍物至少需要两个采集点(圆心和圆周点)"; |
| | | session.captureMessage = "圆形障碍物至少需要三个采集点(圆周上的点)"; |
| | | return; |
| | | } |
| | | double[] center = xyPoints.get(0); |
| | | double[] radiusPoint = xyPoints.get(xyPoints.size() - 1); |
| | | CircleFitResult circle = fitCircleFromPoints(xyPoints); |
| | | if (circle == null) { |
| | | session.captureSuccessful = false; |
| | | session.captureMessage = "无法根据采集的点生成圆,请确保选择了三个非共线的圆周点"; |
| | | return; |
| | | } |
| | | // 使用计算出的半径生成一个真正的圆上点(圆心右侧的点),确保预览时计算的半径和结束绘制时的半径一致 |
| | | double radiusX = circle.centerX + circle.radius; |
| | | double radiusY = circle.centerY; |
| | | String result = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f", |
| | | center[0], center[1], radiusPoint[0], radiusPoint[1]); |
| | | circle.centerX, circle.centerY, radiusX, radiusY); |
| | | session.data.put("obstacleCoordinates", result); |
| | | session.captureSuccessful = true; |
| | | session.captureMessage = "已采集圆形障碍物坐标"; |
| | | session.captureMessage = "已采集圆形障碍物,共 " + xyPoints.size() + " 个点"; |
| | | } else { |
| | | if (xyPoints.size() < 3) { |
| | | session.captureSuccessful = false; |
| | |
| | | return result; |
| | | } |
| | | |
| | | private static String buildOriginalCoordinateString(List<Coordinate> coords) { |
| | | if (coords == null || coords.isEmpty()) { |
| | | return null; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (Coordinate coord : coords) { |
| | | if (coord == null) { |
| | | continue; |
| | | } |
| | | String latToken = sanitizeCoordinateToken(coord.getLatitude()); |
| | | String lonToken = sanitizeCoordinateToken(coord.getLongitude()); |
| | | if (latToken == null || lonToken == null) { |
| | | continue; |
| | | } |
| | | char latDir = sanitizeDirection(coord.getLatDirection(), 'N'); |
| | | char lonDir = sanitizeDirection(coord.getLonDirection(), 'E'); |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(latToken) |
| | | .append(",") |
| | | .append(latDir) |
| | | .append(",") |
| | | .append(lonToken) |
| | | .append(",") |
| | | .append(lonDir); |
| | | } |
| | | return sb.length() > 0 ? sb.toString() : null; |
| | | } |
| | | |
| | | private static String sanitizeCoordinateToken(String token) { |
| | | if (token == null) { |
| | | return null; |
| | | } |
| | | String trimmed = token.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | return null; |
| | | } |
| | | try { |
| | | Double.parseDouble(trimmed); |
| | | return trimmed; |
| | | } catch (NumberFormatException ex) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private static char sanitizeDirection(String direction, char fallback) { |
| | | if (direction == null) { |
| | | return fallback; |
| | | } |
| | | String trimmed = direction.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | return fallback; |
| | | } |
| | | char ch = Character.toUpperCase(trimmed.charAt(0)); |
| | | if (ch != 'N' && ch != 'S' && ch != 'E' && ch != 'W') { |
| | | return fallback; |
| | | } |
| | | return ch; |
| | | } |
| | | |
| | | private static double parseDMToDecimal(String dmm, String direction) { |
| | | if (dmm == null || dmm.trim().isEmpty()) { |
| | | return Double.NaN; |
| | |
| | | return new double[]{eastMeters, northMeters}; |
| | | } |
| | | |
| | | private static CircleFitResult fitCircleFromPoints(List<double[]> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return null; |
| | | } |
| | | CircleFitResult best = null; |
| | | double bestScore = 0.0; |
| | | int n = points.size(); |
| | | for (int i = 0; i < n - 2; i++) { |
| | | double[] p1 = points.get(i); |
| | | for (int j = i + 1; j < n - 1; j++) { |
| | | double[] p2 = points.get(j); |
| | | for (int k = j + 1; k < n; k++) { |
| | | double[] p3 = points.get(k); |
| | | CircleFitResult candidate = circleFromThreePoints(p1, p2, p3); |
| | | if (candidate == null || candidate.radius <= 0) { |
| | | continue; |
| | | } |
| | | double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3))); |
| | | if (minEdge > bestScore) { |
| | | bestScore = minEdge; |
| | | best = candidate; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return best; |
| | | } |
| | | |
| | | private static CircleFitResult circleFromThreePoints(double[] p1, double[] p2, double[] p3) { |
| | | double x1 = p1[0]; |
| | | double y1 = p1[1]; |
| | | double x2 = p2[0]; |
| | | double y2 = p2[1]; |
| | | double x3 = p3[0]; |
| | | double y3 = p3[1]; |
| | | |
| | | double determinant = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)); |
| | | if (Math.abs(determinant) < 1e-6) { |
| | | return null; |
| | | } |
| | | |
| | | double x1Sq = x1 * x1 + y1 * y1; |
| | | double x2Sq = x2 * x2 + y2 * y2; |
| | | double x3Sq = x3 * x3 + y3 * y3; |
| | | |
| | | double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / determinant; |
| | | double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / determinant; |
| | | double radius = Math.hypot(centerX - x1, centerY - y1); |
| | | |
| | | if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { |
| | | return null; |
| | | } |
| | | if (radius < 0.05) { |
| | | return null; |
| | | } |
| | | |
| | | return new CircleFitResult(centerX, centerY, radius, new double[]{x1, y1}); |
| | | } |
| | | |
| | | private static double[] pickRadiusPoint(List<double[]> points, CircleFitResult circle) { |
| | | if (circle == null || points == null || points.isEmpty()) { |
| | | return null; |
| | | } |
| | | double[] best = null; |
| | | double bestDistance = 0.0; |
| | | for (double[] pt : points) { |
| | | double dist = Math.hypot(pt[0] - circle.centerX, pt[1] - circle.centerY); |
| | | if (dist > bestDistance) { |
| | | bestDistance = dist; |
| | | best = pt; |
| | | } |
| | | } |
| | | return best; |
| | | } |
| | | |
| | | private static double distance(double[] a, double[] b) { |
| | | double dx = a[0] - b[0]; |
| | | double dy = a[1] - b[1]; |
| | | return Math.hypot(dx, dy); |
| | | } |
| | | |
| | | private static final class CircleFitResult { |
| | | final double centerX; |
| | | final double centerY; |
| | | final double radius; |
| | | final double[] referencePoint; |
| | | |
| | | CircleFitResult(double centerX, double centerY, double radius, double[] referencePoint) { |
| | | this.centerX = centerX; |
| | | this.centerY = centerY; |
| | | this.radius = radius; |
| | | this.referencePoint = referencePoint; |
| | | } |
| | | } |
| | | |
| | | private List<ExistingObstacle> resolveExistingObstacles(Dikuai target, List<String> providedNames) { |
| | | String landNumber = target != null ? target.getLandNumber() : null; |
| | | Map<String, ExistingObstacle> configMap = loadObstacleDetailsFromConfig(landNumber); |
| | |
| | | } |
| | | if (!configMap.isEmpty()) { |
| | | List<ExistingObstacle> remaining = new ArrayList<>(configMap.values()); |
| | | remaining.sort(Comparator.comparing(ExistingObstacle::getName, String.CASE_INSENSITIVE_ORDER)); |
| | | remaining.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.getName(), b.getName())); |
| | | result.addAll(remaining); |
| | | } |
| | | if (result.isEmpty()) { |
| | |
| | | } |
| | | String trimmedName = name.trim(); |
| | | Obstacledge.ObstacleShape shape = obstacle.getShape(); |
| | | String xyCoords = obstacle.getXyCoordsString(); |
| | | String original = obstacle.getOriginalCoordsString(); |
| | | String coords = isMeaningfulValue(xyCoords) ? xyCoords.trim() |
| | | : (isMeaningfulValue(original) ? original.trim() : ""); |
| | | details.put(trimmedName, new ExistingObstacle(trimmedName, shape, coords)); |
| | | String xyCoords = obstacle.getXyCoordsString(); |
| | | String original = obstacle.getOriginalCoordsString(); |
| | | String xy = isMeaningfulValue(xyCoords) ? xyCoords.trim() : ""; |
| | | String originalTrimmed = isMeaningfulValue(original) ? original.trim() : ""; |
| | | details.put(trimmedName, new ExistingObstacle(trimmedName, shape, xy, originalTrimmed)); |
| | | } |
| | | } catch (Exception ex) { |
| | | System.err.println("加载障碍物详情失败: " + ex.getMessage()); |
| | |
| | | } |
| | | |
| | | private void preloadData() { |
| | | String existing = targetDikuai.getObstacleCoordinates(); |
| | | if (isMeaningfulValue(existing)) { |
| | | formData.put("obstacleCoordinates", existing.trim()); |
| | | } |
| | | formData.remove("obstacleCoordinates"); |
| | | formData.remove("obstacleOriginalCoordinates"); |
| | | formData.remove("generatedBoundaryCoordinates"); // 清除生成的边界坐标 |
| | | formData.remove("editingObstacleName"); |
| | | updateDrawingStatus(); |
| | | updatePreviewButtonState(); |
| | | // 清除边界状态标签 |
| | | if (boundaryStatusLabel != null) { |
| | | boundaryStatusLabel.setVisible(false); |
| | | boundaryStatusLabel.setText(""); |
| | | } |
| | | } |
| | | |
| | | private void updateDrawingStatus() { |
| | |
| | | return; |
| | | } |
| | | String coords = formData.get("obstacleCoordinates"); |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | boolean isCircle = "circle".equals(shapeKey); |
| | | |
| | | if (isMeaningfulValue(coords)) { |
| | | int count = countCoordinatePairs(coords); |
| | | drawingStatusLabel.setText("已采集障碍物数据,点数:" + count); |
| | | if (isCircle) { |
| | | // 对于圆形障碍物,显示圆心坐标和半径 |
| | | String statusText = parseCircleStatusText(coords); |
| | | drawingStatusLabel.setText(statusText); |
| | | } else { |
| | | // 对于多边形障碍物,显示点数 |
| | | int count = countCoordinatePairs(coords); |
| | | drawingStatusLabel.setText("已采集障碍物数据,点数:" + count); |
| | | } |
| | | if (!drawingInProgress && drawButton != null) { |
| | | drawButton.setText("重新绘制"); |
| | | drawButton.setEnabled(true); |
| | | } |
| | | // 对于圆形障碍物,不显示"生成障碍物边界"按钮 |
| | | if (generateBoundaryButton != null) { |
| | | generateBoundaryButton.setVisible(!isCircle); |
| | | } |
| | | } else { |
| | | drawingStatusLabel.setText("尚未采集障碍物坐标"); |
| | | if (!drawingInProgress && drawButton != null) { |
| | | drawButton.setText("开始绘制"); |
| | | drawButton.setEnabled(true); |
| | | } |
| | | // 未绘制时隐藏"生成障碍物边界"按钮 |
| | | if (generateBoundaryButton != null) { |
| | | generateBoundaryButton.setVisible(false); |
| | | } |
| | | // 未生成边界时隐藏预览按钮 |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(false); |
| | | } |
| | | // 隐藏边界状态标签 |
| | | if (boundaryStatusLabel != null) { |
| | | boundaryStatusLabel.setVisible(false); |
| | | } |
| | | } |
| | | updateSaveButtonState(); |
| | | updatePreviewButtonState(); |
| | | } |
| | | |
| | | /** |
| | | * 更新边界状态标签 |
| | | */ |
| | | private void updateBoundaryStatusLabel(int pointCount) { |
| | | if (boundaryStatusLabel == null) { |
| | | return; |
| | | } |
| | | if (pointCount > 0) { |
| | | boundaryStatusLabel.setText("生成障碍物边界点数:" + pointCount); |
| | | boundaryStatusLabel.setVisible(true); |
| | | } else { |
| | | boundaryStatusLabel.setText(""); |
| | | boundaryStatusLabel.setVisible(false); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 更新预览按钮的显示状态 |
| | | */ |
| | | private void updatePreviewButtonState() { |
| | | if (previewButton == null) { |
| | | return; |
| | | } |
| | | |
| | | // 对于圆形障碍物,不显示预览按钮 |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | boolean isCircle = "circle".equals(shapeKey); |
| | | if (isCircle) { |
| | | previewButton.setVisible(false); |
| | | previewButton.setEnabled(false); |
| | | return; |
| | | } |
| | | |
| | | // 只有在生成边界后才显示预览按钮 |
| | | String generatedBoundary = formData.get("generatedBoundaryCoordinates"); |
| | | boolean hasGeneratedBoundary = isMeaningfulValue(generatedBoundary); |
| | | |
| | | if (hasGeneratedBoundary) { |
| | | // 生成边界后,重新创建绿色的预览按钮 |
| | | // 获取按钮的父容器和位置 |
| | | Container parent = previewButton.getParent(); |
| | | if (parent != null) { |
| | | // 保存原有的ActionListener |
| | | ActionListener[] listeners = previewButton.getActionListeners(); |
| | | |
| | | // 移除旧按钮 |
| | | parent.remove(previewButton); |
| | | |
| | | // 创建新的绿色预览按钮(使用主按钮样式) |
| | | previewButton = createPrimaryButton("预览", 16); |
| | | previewButton.setVisible(true); |
| | | previewButton.setEnabled(true); |
| | | |
| | | // 恢复ActionListener |
| | | for (ActionListener listener : listeners) { |
| | | previewButton.addActionListener(listener); |
| | | } |
| | | |
| | | // 添加到父容器(在保存按钮之前) |
| | | int previewIndex = -1; |
| | | Component[] components = parent.getComponents(); |
| | | for (int i = 0; i < components.length; i++) { |
| | | if (components[i] instanceof JButton) { |
| | | JButton btn = (JButton) components[i]; |
| | | if ("保存".equals(btn.getText())) { |
| | | previewIndex = i; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (previewIndex >= 0) { |
| | | parent.add(previewButton, previewIndex); |
| | | parent.add(Box.createRigidArea(new Dimension(10, 0)), previewIndex + 1); |
| | | } else { |
| | | // 如果找不到保存按钮,添加到末尾 |
| | | parent.add(Box.createRigidArea(new Dimension(12, 0))); |
| | | parent.add(previewButton); |
| | | parent.add(Box.createRigidArea(new Dimension(10, 0))); |
| | | } |
| | | |
| | | // 刷新布局 |
| | | parent.revalidate(); |
| | | parent.repaint(); |
| | | } |
| | | } else { |
| | | // 未生成边界时,隐藏按钮 |
| | | previewButton.setVisible(false); |
| | | previewButton.setEnabled(false); |
| | | } |
| | | } |
| | | |
| | | private int countCoordinatePairs(String coords) { |
| | |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | /** |
| | | * 解析圆形障碍物坐标,生成状态文本 |
| | | * 格式:centerX,centerY;radiusX,radiusY |
| | | * 返回:当前采集圆形圆心坐标x,y,半径n米 |
| | | */ |
| | | private String parseCircleStatusText(String coords) { |
| | | if (!isMeaningfulValue(coords)) { |
| | | return "尚未采集圆形障碍物坐标"; |
| | | } |
| | | |
| | | try { |
| | | String[] pairs = coords.split(";"); |
| | | if (pairs.length < 2) { |
| | | return "圆形障碍物坐标格式错误"; |
| | | } |
| | | |
| | | // 解析圆心坐标 |
| | | String[] centerParts = pairs[0].trim().split(","); |
| | | if (centerParts.length < 2) { |
| | | return "圆形障碍物坐标格式错误"; |
| | | } |
| | | double centerX = Double.parseDouble(centerParts[0].trim()); |
| | | double centerY = Double.parseDouble(centerParts[1].trim()); |
| | | |
| | | // 解析圆上一点坐标 |
| | | String[] radiusParts = pairs[1].trim().split(","); |
| | | if (radiusParts.length < 2) { |
| | | return "圆形障碍物坐标格式错误"; |
| | | } |
| | | double radiusX = Double.parseDouble(radiusParts[0].trim()); |
| | | double radiusY = Double.parseDouble(radiusParts[1].trim()); |
| | | |
| | | // 计算半径(米) |
| | | double radius = Math.sqrt(Math.pow(radiusX - centerX, 2) + Math.pow(radiusY - centerY, 2)); |
| | | |
| | | // 格式化显示:当前采集圆形圆心坐标x,y,半径n米 |
| | | return String.format(Locale.US, "当前采集圆形圆心坐标%.2f,%.2f,半径%.2f米", |
| | | centerX, centerY, radius); |
| | | } catch (Exception e) { |
| | | return "圆形障碍物坐标解析失败"; |
| | | } |
| | | } |
| | | |
| | | private void updateSaveButtonState() { |
| | | if (saveButton != null) { |
| | | saveButton.setEnabled(isMeaningfulValue(formData.get("obstacleCoordinates"))); |
| | | boolean hasCoords = isMeaningfulValue(formData.get("obstacleCoordinates")); |
| | | boolean hasName = isMeaningfulValue(formData.get("obstacleName")); |
| | | |
| | | // 检查是否生成了障碍物边界坐标 |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | boolean hasGeneratedBoundary = isMeaningfulValue(formData.get("generatedBoundaryCoordinates")); |
| | | |
| | | // 如果是多边形障碍物,必须生成边界坐标才能保存 |
| | | // 如果是圆形障碍物或其他形状,不需要生成边界就能保存 |
| | | boolean boundaryRequirementMet = true; |
| | | if ("polygon".equals(shapeKey)) { |
| | | boundaryRequirementMet = hasGeneratedBoundary; |
| | | } |
| | | |
| | | boolean enabled = hasCoords && hasName && boundaryRequirementMet; |
| | | saveButton.setEnabled(enabled); |
| | | if (enabled) { |
| | | saveButton.setBackground(PRIMARY_COLOR); |
| | | saveButton.setForeground(WHITE); |
| | | saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | saveButton.setOpaque(true); |
| | | saveButton.setContentAreaFilled(true); |
| | | } else { |
| | | saveButton.setBackground(MEDIUM_GRAY); |
| | | saveButton.setForeground(TEXT_COLOR); |
| | | saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); |
| | | saveButton.setOpaque(true); |
| | | saveButton.setContentAreaFilled(true); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private boolean validateStep2() { |
| | | String name = formData.get("obstacleName"); |
| | | if (!isMeaningfulValue(name)) { |
| | | JOptionPane.showMessageDialog(this, "请填写障碍物名称", "提示", JOptionPane.WARNING_MESSAGE); |
| | | showStep(1); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.requestFocusInWindow(); |
| | | } |
| | | return false; |
| | | } |
| | | if (!isObstacleNameUnique(name)) { |
| | | JOptionPane.showMessageDialog(this, "障碍物名称已存在,请输入唯一名称", "提示", JOptionPane.WARNING_MESSAGE); |
| | | showStep(1); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.requestFocusInWindow(); |
| | | } |
| | | return false; |
| | | } |
| | | if (!isMeaningfulValue(formData.get("obstacleCoordinates"))) { |
| | | JOptionPane.showMessageDialog(this, "请先完成障碍物绘制", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | |
| | | if (!validateStep2()) { |
| | | return; |
| | | } |
| | | String coords = formData.get("obstacleCoordinates").trim(); |
| | | String landNumber = targetDikuai.getLandNumber(); |
| | | if (!Dikuai.updateField(landNumber, "obstacleCoordinates", coords)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | String obstacleName = formData.get("obstacleName"); |
| | | String coordsValue = formData.get("obstacleCoordinates"); |
| | | String originalValue = formData.get("obstacleOriginalCoordinates"); |
| | | String generatedBoundaryValue = formData.get("generatedBoundaryCoordinates"); // 生成的边界坐标 |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | String previousName = formData.get("editingObstacleName"); |
| | | |
| | | if (!isMeaningfulValue(coordsValue)) { |
| | | JOptionPane.showMessageDialog(this, "障碍物坐标无效", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | String coords = coordsValue.trim(); |
| | | String originalCoords = isMeaningfulValue(originalValue) ? originalValue.trim() : null; |
| | | // 对于圆形障碍物,直接使用obstacleCoordinates(在第3个点确认时已生成) |
| | | // 对于多边形障碍物,如果有生成的边界坐标,使用生成的边界坐标;否则使用原始坐标 |
| | | String finalCoords; |
| | | if ("circle".equals(shapeKey)) { |
| | | // 圆形障碍物直接使用已生成的坐标 |
| | | finalCoords = coords; |
| | | } else { |
| | | // 多边形障碍物优先使用生成的边界坐标 |
| | | finalCoords = isMeaningfulValue(generatedBoundaryValue) ? generatedBoundaryValue.trim() : coords; |
| | | } |
| | | String trimmedName = isMeaningfulValue(obstacleName) ? obstacleName.trim() : null; |
| | | if (!isMeaningfulValue(trimmedName)) { |
| | | JOptionPane.showMessageDialog(this, "障碍物名称无效", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | formData.put("obstacleName", trimmedName); |
| | | if (isMeaningfulValue(previousName)) { |
| | | formData.put("editingObstacleName", trimmedName); |
| | | } |
| | | |
| | | if (!persistObstacleToConfig(landNumber, previousName, trimmedName, shapeKey, finalCoords, originalCoords)) { |
| | | JOptionPane.showMessageDialog(this, "写入障碍物配置失败,请重试", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | Dikuai.updateField(landNumber, "updateTime", currentTime()); |
| | | Dikuai.saveToProperties(); |
| | | Dikuaiguanli.notifyExternalCreation(landNumber); |
| | | |
| | | JOptionPane.showMessageDialog(this, "障碍物坐标已保存", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | JOptionPane.showMessageDialog(this, "障碍物数据已保存", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | activeSession = null; |
| | | dispose(); |
| | | } |
| | | |
| | | /** |
| | | * 生成障碍物边界坐标 |
| | | */ |
| | | private void generateObstacleBoundary() { |
| | | String originalCoords = formData.get("obstacleOriginalCoordinates"); |
| | | if (!isMeaningfulValue(originalCoords)) { |
| | | JOptionPane.showMessageDialog(this, "无原始坐标数据,无法生成边界", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | String baseStation = targetDikuai.getBaseStationCoordinates(); |
| | | if (!isMeaningfulValue(baseStation)) { |
| | | JOptionPane.showMessageDialog(this, "地块未设置基站坐标", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | |
| | | try { |
| | | String method = formData.get("drawingMethod"); |
| | | |
| | | // 处理圆形障碍物 |
| | | if ("circle".equals(shapeKey)) { |
| | | if (!"mower".equals(method)) { |
| | | JOptionPane.showMessageDialog(this, "只有割草机绘制的圆形障碍物才支持生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 将原始坐标转换为Coordinate对象列表 |
| | | List<Coordinate> coordinateList = parseOriginalCoordinatesToCoordinateList(originalCoords); |
| | | if (coordinateList.size() < 3) { |
| | | JOptionPane.showMessageDialog(this, "圆形障碍物至少需要三个采集点", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 解析基站坐标 |
| | | String[] baseParts = baseStation.split(","); |
| | | if (baseParts.length < 4) { |
| | | JOptionPane.showMessageDialog(this, "基站坐标格式无效", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | double baseLat = parseDMToDecimal(baseParts[0].trim(), baseParts[1].trim()); |
| | | double baseLon = parseDMToDecimal(baseParts[2].trim(), baseParts[3].trim()); |
| | | |
| | | // 将原始坐标转换为XY坐标(本地坐标系) |
| | | List<double[]> xyPoints = new ArrayList<>(); |
| | | for (Coordinate coord : coordinateList) { |
| | | double lat = parseDMToDecimal(coord.getLatitude(), coord.getLatDirection()); |
| | | double lon = parseDMToDecimal(coord.getLongitude(), coord.getLonDirection()); |
| | | xyPoints.add(convertLatLonToLocal(lat, lon, baseLat, baseLon)); |
| | | } |
| | | |
| | | // 使用ThreePointCircle算法计算圆心和半径 |
| | | if (xyPoints.size() < 3) { |
| | | JOptionPane.showMessageDialog(this, "至少需要三个点才能生成圆形边界", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 取前三个点计算圆 |
| | | double[] p1 = xyPoints.get(0); |
| | | double[] p2 = xyPoints.get(1); |
| | | double[] p3 = xyPoints.get(2); |
| | | |
| | | String point1 = String.format(Locale.US, "%.2f,%.2f", p1[0], p1[1]); |
| | | String point2 = String.format(Locale.US, "%.2f,%.2f", p2[0], p2[1]); |
| | | String point3 = String.format(Locale.US, "%.2f,%.2f", p3[0], p3[1]); |
| | | |
| | | String circleResult = bianjie.ThreePointCircle.getCircleFromPoints(point1, point2, point3); |
| | | |
| | | // 解析结果:格式为 "圆心: x,y; 半径: r" |
| | | if (circleResult == null || circleResult.startsWith("错误")) { |
| | | JOptionPane.showMessageDialog(this, "生成圆形边界失败: " + circleResult, "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 解析圆心和半径 |
| | | String[] parts = circleResult.split(";"); |
| | | if (parts.length < 2) { |
| | | JOptionPane.showMessageDialog(this, "解析圆形边界结果失败", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 提取圆心坐标 |
| | | String centerPart = parts[0].trim(); // "圆心: x,y" |
| | | String radiusPart = parts[1].trim(); // "半径: r" |
| | | |
| | | String centerCoords = centerPart.substring(centerPart.indexOf(":") + 1).trim(); |
| | | String radiusStr = radiusPart.substring(radiusPart.indexOf(":") + 1).trim(); |
| | | |
| | | String[] centerXY = centerCoords.split(","); |
| | | if (centerXY.length < 2) { |
| | | JOptionPane.showMessageDialog(this, "解析圆心坐标失败", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | double centerX = Double.parseDouble(centerXY[0].trim()); |
| | | double centerY = Double.parseDouble(centerXY[1].trim()); |
| | | double radius = Double.parseDouble(radiusStr.trim()); |
| | | |
| | | // 计算圆上一点(圆心右侧的点) |
| | | double radiusX = centerX + radius; |
| | | double radiusY = centerY; |
| | | |
| | | // 生成边界坐标格式:圆心X,圆心Y;圆上点X,圆上点Y |
| | | String boundaryCoords = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f", |
| | | centerX, centerY, radiusX, radiusY); |
| | | |
| | | // 保存生成的边界坐标 |
| | | formData.put("generatedBoundaryCoordinates", boundaryCoords); |
| | | |
| | | // 更新边界状态标签显示(圆形只有2个点:圆心和圆上一点) |
| | | updateBoundaryStatusLabel(2); |
| | | |
| | | // 更新预览按钮显示(变成绿色可点击) |
| | | updatePreviewButtonState(); |
| | | |
| | | // 更新保存按钮状态(变成可点击) |
| | | updateSaveButtonState(); |
| | | |
| | | // 强制刷新UI |
| | | SwingUtilities.invokeLater(() -> { |
| | | if (previewButton != null) { |
| | | previewButton.revalidate(); |
| | | previewButton.repaint(); |
| | | } |
| | | }); |
| | | |
| | | JOptionPane.showMessageDialog(this, "圆形障碍物边界坐标已生成", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 处理多边形障碍物 |
| | | if (!"polygon".equals(shapeKey)) { |
| | | JOptionPane.showMessageDialog(this, "只有多边形或圆形障碍物才需要生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 检查绘制方式,只有割草机绘制的多边形才调用bianjieguihua2 |
| | | if (!"mower".equals(method)) { |
| | | JOptionPane.showMessageDialog(this, "只有割草机绘制的多边形才支持生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 将原始坐标转换为Coordinate对象列表 |
| | | List<Coordinate> coordinateList = parseOriginalCoordinatesToCoordinateList(originalCoords); |
| | | if (coordinateList.isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, "原始坐标数据无效", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 保存当前的Coordinate.coordinates |
| | | List<Coordinate> savedCoordinates = new ArrayList<>(Coordinate.coordinates); |
| | | |
| | | try { |
| | | // 设置到全局坐标列表 |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.coordinates.addAll(coordinateList); |
| | | |
| | | // 调用bianjieguihua2算法生成优化后的多边形边界坐标 |
| | | String optimizedCoordsStr = bianjieguihua2.processCoordinateListAuto(baseStation); |
| | | |
| | | if (optimizedCoordsStr == null || optimizedCoordsStr.trim().isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, "生成边界坐标失败", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 保存生成的边界坐标 |
| | | formData.put("generatedBoundaryCoordinates", optimizedCoordsStr.trim()); |
| | | |
| | | // 计算生成的边界点数 |
| | | int boundaryPointCount = countCoordinatePairs(optimizedCoordsStr.trim()); |
| | | |
| | | // 更新边界状态标签显示 |
| | | updateBoundaryStatusLabel(boundaryPointCount); |
| | | |
| | | // 更新预览按钮显示(变成绿色可点击) |
| | | updatePreviewButtonState(); |
| | | |
| | | // 更新保存按钮状态(变成可点击) |
| | | updateSaveButtonState(); |
| | | |
| | | // 强制刷新UI |
| | | SwingUtilities.invokeLater(() -> { |
| | | if (previewButton != null) { |
| | | previewButton.revalidate(); |
| | | previewButton.repaint(); |
| | | } |
| | | }); |
| | | |
| | | JOptionPane.showMessageDialog(this, "障碍物边界坐标已生成", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | |
| | | } finally { |
| | | // 恢复原来的坐标列表 |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.coordinates.addAll(savedCoordinates); |
| | | } |
| | | |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | JOptionPane.showMessageDialog(this, "生成边界坐标时发生错误: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将原始坐标字符串解析为Coordinate对象列表 |
| | | */ |
| | | private List<Coordinate> parseOriginalCoordinatesToCoordinateList(String originalCoords) { |
| | | List<Coordinate> coordinateList = new ArrayList<>(); |
| | | if (!isMeaningfulValue(originalCoords)) { |
| | | return coordinateList; |
| | | } |
| | | |
| | | // 原始坐标格式:纬度1,方向1,经度1,方向1;纬度2,方向2,经度2,方向2;... |
| | | String[] pointStrings = originalCoords.split(";"); |
| | | for (String pointStr : pointStrings) { |
| | | pointStr = pointStr.trim(); |
| | | if (pointStr.isEmpty()) continue; |
| | | |
| | | String[] parts = pointStr.split(","); |
| | | if (parts.length >= 4) { |
| | | try { |
| | | String lat = parts[0].trim(); |
| | | String latDir = parts[1].trim(); |
| | | String lon = parts[2].trim(); |
| | | String lonDir = parts[3].trim(); |
| | | |
| | | Coordinate coord = new Coordinate(lat, latDir, lon, lonDir, 0.0); |
| | | coordinateList.add(coord); |
| | | } catch (Exception e) { |
| | | // 跳过无效的坐标点 |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return coordinateList; |
| | | } |
| | | |
| | | /** |
| | | * 预览障碍物边界 |
| | | */ |
| | | private void previewObstacleBoundary() { |
| | | String generatedBoundary = formData.get("generatedBoundaryCoordinates"); |
| | | if (!isMeaningfulValue(generatedBoundary)) { |
| | | JOptionPane.showMessageDialog(this, "请先生成障碍物边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | String landNumber = targetDikuai.getLandNumber(); |
| | | String landName = targetDikuai.getLandName(); |
| | | String boundary = targetDikuai.getBoundaryCoordinates(); |
| | | |
| | | // 获取障碍物坐标(优先使用生成的边界坐标,如果没有则使用原始坐标) |
| | | String obstacleCoords = generatedBoundary; |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | String obstacleName = formData.get("obstacleName"); |
| | | |
| | | // 对于圆形障碍物,生成的边界坐标格式就是障碍物坐标格式,可以直接使用 |
| | | // 对于多边形障碍物,也需要使用生成的边界坐标 |
| | | // 如果生成的边界坐标不可用,尝试使用原始障碍物坐标 |
| | | if (!isMeaningfulValue(obstacleCoords)) { |
| | | obstacleCoords = formData.get("obstacleCoordinates"); |
| | | if (!isMeaningfulValue(obstacleCoords)) { |
| | | JOptionPane.showMessageDialog(this, "无法获取障碍物坐标进行预览", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 构建障碍物数据字符串,包含名称、形状和坐标 |
| | | // 格式:障碍物名称::形状::坐标 或 障碍物名称:形状:坐标 |
| | | final String obstacleData; |
| | | if (isMeaningfulValue(obstacleName) && isMeaningfulValue(shapeKey)) { |
| | | // 使用 :: 分隔符格式:名称::形状::坐标 |
| | | obstacleData = obstacleName.trim() + "::" + shapeKey.trim() + "::" + obstacleCoords; |
| | | } else if (isMeaningfulValue(shapeKey)) { |
| | | // 只有形状:形状::坐标 |
| | | obstacleData = shapeKey.trim() + "::" + obstacleCoords; |
| | | } else { |
| | | // 只有坐标 |
| | | obstacleData = obstacleCoords; |
| | | } |
| | | |
| | | // 关闭当前对话框 |
| | | setVisible(false); |
| | | |
| | | SwingUtilities.invokeLater(() -> { |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | // 传递回调以重新打开新增障碍物步骤2页面 |
| | | shouye.startMowingPathPreview( |
| | | landNumber, |
| | | landName, |
| | | boundary, |
| | | obstacleData, // 使用包含名称和形状的障碍物数据 |
| | | null, |
| | | () -> SwingUtilities.invokeLater(() -> { |
| | | // 重新打开新增障碍物步骤2页面 |
| | | Window owner = SwingUtilities.getWindowAncestor(shouye); |
| | | setVisible(true); |
| | | }) |
| | | ); |
| | | } else { |
| | | JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE); |
| | | setVisible(true); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private boolean persistObstacleToConfig(String landNumber, String previousName, String obstacleName, |
| | | String shapeKey, String xyCoords, String originalCoords) { |
| | | if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName) || !isMeaningfulValue(xyCoords)) { |
| | | return false; |
| | | } |
| | | |
| | | String normalizedLandNumber = landNumber.trim(); |
| | | String normalizedNewName = obstacleName.trim(); |
| | | String normalizedPrevious = isMeaningfulValue(previousName) ? previousName.trim() : null; |
| | | |
| | | String normalizedXy = xyCoords.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(normalizedLandNumber); |
| | | if (plot == null) { |
| | | plot = new Obstacledge.Plot(normalizedLandNumber); |
| | | manager.addPlot(plot); |
| | | } |
| | | |
| | | ensurePlotBaseStation(plot); |
| | | |
| | | if (normalizedPrevious != null && !normalizedPrevious.equals(normalizedNewName)) { |
| | | plot.removeObstacleByName(normalizedPrevious); |
| | | } |
| | | |
| | | Obstacledge.Obstacle obstacle = plot.getObstacleByName(normalizedNewName); |
| | | if (obstacle == null) { |
| | | obstacle = new Obstacledge.Obstacle(normalizedLandNumber, normalizedNewName, |
| | | determineObstacleShape(shapeKey, normalizedXy)); |
| | | plot.addObstacle(obstacle); |
| | | } |
| | | |
| | | obstacle.setPlotId(normalizedLandNumber); |
| | | obstacle.setObstacleName(normalizedNewName); |
| | | obstacle.setShape(determineObstacleShape(shapeKey, normalizedXy)); |
| | | |
| | | obstacle.setXyCoordinates(new ArrayList<Obstacledge.XYCoordinate>()); |
| | | obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>()); |
| | | |
| | | obstacle.setXyCoordsString(normalizedXy); |
| | | |
| | | if (isMeaningfulValue(originalCoords)) { |
| | | try { |
| | | obstacle.setOriginalCoordsString(originalCoords); |
| | | } catch (Exception parseEx) { |
| | | System.err.println("解析障碍物原始坐标失败,将使用占位值: " + parseEx.getMessage()); |
| | | obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>()); |
| | | } |
| | | } |
| | | |
| | | ensurePlaceholderOriginalCoords(obstacle); |
| | | |
| | | return manager.saveToFile(configFile.getAbsolutePath()); |
| | | } catch (Exception ex) { |
| | | System.err.println("保存障碍物配置失败: " + ex.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private Obstacledge.ObstacleShape determineObstacleShape(String shapeKey, String xyCoords) { |
| | | if (isMeaningfulValue(shapeKey)) { |
| | | String normalized = shapeKey.trim().toLowerCase(Locale.ROOT); |
| | | if ("circle".equals(normalized) || "圆形".equals(normalized) || "0".equals(normalized)) { |
| | | return Obstacledge.ObstacleShape.CIRCLE; |
| | | } |
| | | if ("polygon".equals(normalized) || "多边形".equals(normalized) || "1".equals(normalized)) { |
| | | return Obstacledge.ObstacleShape.POLYGON; |
| | | } |
| | | } |
| | | |
| | | int pairCount = countCoordinatePairs(xyCoords); |
| | | if (pairCount <= 0) { |
| | | return Obstacledge.ObstacleShape.POLYGON; |
| | | } |
| | | if (pairCount <= 2) { |
| | | return Obstacledge.ObstacleShape.CIRCLE; |
| | | } |
| | | return Obstacledge.ObstacleShape.POLYGON; |
| | | } |
| | | |
| | | private void ensurePlaceholderOriginalCoords(Obstacledge.Obstacle obstacle) { |
| | | if (obstacle == null) { |
| | | return; |
| | | } |
| | | List<Obstacledge.DMCoordinate> originals = obstacle.getOriginalCoordinates(); |
| | | if (originals != null && !originals.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Obstacledge.XYCoordinate> xyCoords = obstacle.getXyCoordinates(); |
| | | int pointCount = (xyCoords != null && !xyCoords.isEmpty()) ? xyCoords.size() : 1; |
| | | List<Obstacledge.DMCoordinate> placeholders = new ArrayList<>(pointCount * 2); |
| | | for (int i = 0; i < pointCount; i++) { |
| | | placeholders.add(new Obstacledge.DMCoordinate(0.0, 'N')); |
| | | placeholders.add(new Obstacledge.DMCoordinate(0.0, 'E')); |
| | | } |
| | | obstacle.setOriginalCoordinates(placeholders); |
| | | } |
| | | |
| | | private void ensurePlotBaseStation(Obstacledge.Plot plot) { |
| | | if (plot == null) { |
| | | return; |
| | | } |
| | | String existing = plot.getBaseStationString(); |
| | | if (isMeaningfulValue(existing)) { |
| | | return; |
| | | } |
| | | String baseStation = resolveBaseStationCoordinates(); |
| | | if (!isMeaningfulValue(baseStation)) { |
| | | return; |
| | | } |
| | | try { |
| | | plot.setBaseStationString(baseStation.trim()); |
| | | } catch (Exception ex) { |
| | | System.err.println("设置基准站坐标失败: " + ex.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private static final class ExistingObstacle { |
| | | private final String name; |
| | | private final Obstacledge.ObstacleShape shape; |
| | | private final String coordinates; |
| | | private final String xyCoordinates; |
| | | private final String originalCoordinates; |
| | | |
| | | ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String coordinates) { |
| | | ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String xyCoordinates, String originalCoordinates) { |
| | | this.name = name != null ? name : ""; |
| | | this.shape = shape; |
| | | this.coordinates = coordinates != null ? coordinates : ""; |
| | | this.xyCoordinates = safeCoordString(xyCoordinates); |
| | | this.originalCoordinates = safeCoordString(originalCoordinates); |
| | | } |
| | | |
| | | static ExistingObstacle placeholder(String name) { |
| | | return new ExistingObstacle(name, null, ""); |
| | | return new ExistingObstacle(name, null, "", ""); |
| | | } |
| | | |
| | | String getName() { |
| | |
| | | } |
| | | |
| | | String getCoordinates() { |
| | | return coordinates; |
| | | return getDisplayCoordinates(); |
| | | } |
| | | |
| | | String getDisplayCoordinates() { |
| | | return hasText(xyCoordinates) ? xyCoordinates : originalCoordinates; |
| | | } |
| | | |
| | | String getXyCoordinates() { |
| | | return xyCoordinates; |
| | | } |
| | | |
| | | String getOriginalCoordinates() { |
| | | return originalCoordinates; |
| | | } |
| | | |
| | | boolean hasPersistedData() { |
| | | return hasText(xyCoordinates) || hasText(originalCoordinates); |
| | | } |
| | | |
| | | private static String safeCoordString(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | return value.trim(); |
| | | } |
| | | |
| | | private static boolean hasText(String value) { |
| | | if (value == null) { |
| | | return false; |
| | | } |
| | | String trimmed = value.trim(); |
| | | return !trimmed.isEmpty() && !"-1".equals(trimmed); |
| | | } |
| | | } |
| | | |
| | |
| | | if (step < 2) { |
| | | nextButton.setVisible(true); |
| | | saveButton.setVisible(false); |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(false); |
| | | } |
| | | } else { |
| | | nextButton.setVisible(false); |
| | | saveButton.setVisible(true); |
| | | updateDrawingStatus(); |
| | | updatePreviewButtonState(); |
| | | // 对于圆形障碍物,确保预览按钮和生成边界按钮隐藏 |
| | | String shapeKey = formData.get("obstacleShape"); |
| | | boolean isCircle = "circle".equals(shapeKey); |
| | | if (isCircle) { |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(false); |
| | | } |
| | | if (generateBoundaryButton != null) { |
| | | generateBoundaryButton.setVisible(false); |
| | | } |
| | | } |
| | | } |
| | | updateSaveButtonState(); |
| | | revalidate(); |
| | |
| | | formData.clear(); |
| | | formData.putAll(session.data); |
| | | |
| | | String sessionName = session.data.get("obstacleName"); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.setText(sessionName != null ? sessionName : ""); |
| | | } |
| | | |
| | | String method = session.data.get("drawingMethod"); |
| | | if (method != null) { |
| | | JPanel panel = methodOptionPanels.get(method); |
| | |
| | | } |
| | | } |
| | | |
| | | if (session.captureMessage != null) { |
| | | if (session.captureMessage != null && !session.captureSuccessful) { |
| | | JOptionPane.showMessageDialog(this, |
| | | session.captureMessage, |
| | | session.captureSuccessful ? "成功" : "提示", |
| | |
| | | } |
| | | |
| | | updateDrawingStatus(); |
| | | |
| | | // 如果已有生成的边界坐标,更新边界状态标签 |
| | | String generatedBoundary = session.data.get("generatedBoundaryCoordinates"); |
| | | if (isMeaningfulValue(generatedBoundary)) { |
| | | int boundaryPointCount = countCoordinatePairs(generatedBoundary); |
| | | updateBoundaryStatusLabel(boundaryPointCount); |
| | | } else { |
| | | // 如果没有生成的边界坐标,隐藏边界状态标签 |
| | | if (boundaryStatusLabel != null) { |
| | | boundaryStatusLabel.setVisible(false); |
| | | boundaryStatusLabel.setText(""); |
| | | } |
| | | } |
| | | |
| | | // 更新按钮状态 |
| | | updatePreviewButtonState(); |
| | | updateSaveButtonState(); |
| | | |
| | | currentStep = 2; |
| | | showStep(2); |
| | | } |
| | |
| | | return createInfoRow(label, value, null); |
| | | } |
| | | |
| | | private JPanel createObstacleNameRow() { |
| | | JPanel row = new JPanel(); |
| | | row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); |
| | | row.setOpaque(false); |
| | | row.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36)); |
| | | |
| | | JLabel label = new JLabel("障碍物名称:"); |
| | | label.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | label.setForeground(TEXT_COLOR); |
| | | label.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | if (obstacleNameField == null) { |
| | | obstacleNameField = new JTextField(); |
| | | obstacleNameField.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | obstacleNameField.setColumns(16); |
| | | Dimension fieldSize = new Dimension(240, 30); |
| | | obstacleNameField.setPreferredSize(fieldSize); |
| | | obstacleNameField.setMinimumSize(fieldSize); |
| | | obstacleNameField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | obstacleNameField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 1), |
| | | BorderFactory.createEmptyBorder(4, 8, 4, 8))); |
| | | obstacleNameField.getDocument().addDocumentListener(new DocumentListener() { |
| | | @Override |
| | | public void insertUpdate(DocumentEvent e) { |
| | | handleObstacleNameChanged(); |
| | | } |
| | | |
| | | @Override |
| | | public void removeUpdate(DocumentEvent e) { |
| | | handleObstacleNameChanged(); |
| | | } |
| | | |
| | | @Override |
| | | public void changedUpdate(DocumentEvent e) { |
| | | handleObstacleNameChanged(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | String existing = formData.get("obstacleName"); |
| | | if (existing != null && !existing.equals(obstacleNameField.getText())) { |
| | | obstacleNameField.setText(existing); |
| | | } |
| | | |
| | | row.add(label); |
| | | row.add(Box.createRigidArea(new Dimension(10, 0))); |
| | | row.add(obstacleNameField); |
| | | row.add(Box.createHorizontalGlue()); |
| | | return row; |
| | | } |
| | | |
| | | private void handleObstacleNameChanged() { |
| | | if (obstacleNameField == null) { |
| | | return; |
| | | } |
| | | String text = obstacleNameField.getText(); |
| | | if (isMeaningfulValue(text)) { |
| | | formData.put("obstacleName", text.trim()); |
| | | } else { |
| | | formData.remove("obstacleName"); |
| | | } |
| | | updateSaveButtonState(); |
| | | } |
| | | |
| | | private boolean isObstacleNameUnique(String candidate) { |
| | | if (!isMeaningfulValue(candidate)) { |
| | | return false; |
| | | } |
| | | String trimmed = candidate.trim().toLowerCase(Locale.ROOT); |
| | | String original = formData.get("editingObstacleName"); |
| | | if (isMeaningfulValue(original) && trimmed.equals(original.trim().toLowerCase(Locale.ROOT))) { |
| | | return true; |
| | | } |
| | | for (ExistingObstacle obstacle : existingObstacles) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String existingName = obstacle.getName(); |
| | | if (isMeaningfulValue(existingName) && trimmed.equals(existingName.trim().toLowerCase(Locale.ROOT))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private JPanel createInfoRow(String label, String value, String tooltip) { |
| | | JPanel row = new JPanel(); |
| | | row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); |
| | |
| | | } |
| | | |
| | | private JButton createPrimaryButton(String text, int fontSize) { |
| | | JButton button = new JButton(text); |
| | | JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR); |
| | | button.setFont(new Font("微软雅黑", Font.BOLD, fontSize)); |
| | | button.setBackground(PRIMARY_COLOR); |
| | | button.setForeground(WHITE); |
| | | button.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(PRIMARY_DARK, 2), |
| | | BorderFactory.createEmptyBorder(10, 22, 10, 22))); |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.setFocusPainted(false); |
| | | button.setOpaque(true); |
| | | button.setContentAreaFilled(true); |
| | | button.addMouseListener(new MouseAdapter() { |
| | | @Override |
| | | public void mouseEntered(MouseEvent e) { |
| | |
| | | public void mouseExited(MouseEvent e) { |
| | | if (button.isEnabled()) { |
| | | button.setBackground(PRIMARY_COLOR); |
| | | } else { |
| | | // 禁用时保持灰色背景 |
| | | button.setBackground(MEDIUM_GRAY); |
| | | } |
| | | } |
| | | }); |
| | |
| | | } |
| | | |
| | | private JButton createSecondaryButton(String text) { |
| | | JButton button = new JButton(text); |
| | | JButton button = buttonset.createStyledButton(text, MEDIUM_GRAY); |
| | | button.setFont(new Font("微软雅黑", Font.BOLD, 16)); |
| | | button.setBackground(MEDIUM_GRAY); |
| | | button.setForeground(TEXT_COLOR); |
| | | button.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 22, 10, 22))); |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.setFocusPainted(false); |
| | | button.setOpaque(true); |
| | | button.setContentAreaFilled(true); |
| | | return button; |
| | | } |
| | | |
| | |
| | | return null; |
| | | } |
| | | |
| | | private String resolveDrawingDeviceId(String method) { |
| | | if (!isMeaningfulValue(method)) { |
| | | return null; |
| | | } |
| | | String key = "handheld".equalsIgnoreCase(method) ? "handheldMarkerId" : "mowerId"; |
| | | String value = Setsys.getPropertyValue(key); |
| | | if (!isMeaningfulValue(value)) { |
| | | return null; |
| | | } |
| | | return value.trim(); |
| | | } |
| | | |
| | | private String safeValue(String value, String fallback) { |
| | | if (!isMeaningfulValue(value)) { |
| | | return fallback; |