| | |
| | | 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.Shouye; |
| | | import zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhangaiwu.yulanzhangaiwu; |
| | | |
| | | /** |
| | | * 障碍物新增/编辑对话框。设计语言参考 {@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 selectedShapePanel; |
| | | private JButton drawButton; |
| | | private JLabel drawingStatusLabel; |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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"); |
| | | if (obstacleNameField != null) { |
| | | obstacleNameField.setText(""); |
| | | } else { |
| | | formData.remove("obstacleName"); |
| | | } |
| | | updateDrawingStatus(); |
| | | updateSaveButtonState(); |
| | | } |
| | | } |
| | | |
| | | 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))); |
| | |
| | | |
| | | buttonPanel.add(prevButton); |
| | | buttonPanel.add(Box.createHorizontalGlue()); |
| | | buttonPanel.add(nextButton); |
| | | buttonPanel.add(Box.createRigidArea(new Dimension(12, 0))); |
| | | buttonPanel.add(saveButton); |
| | | |
| | | attachNextButtonToStep1Row(); |
| | | |
| | | return buttonPanel; |
| | | } |
| | | |
| | |
| | | |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | | |
| | | 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[] radiusPoint = pickRadiusPoint(xyPoints, circle); |
| | | if (radiusPoint == null) { |
| | | session.captureSuccessful = false; |
| | | session.captureMessage = "采集的圆周点异常,无法生成圆"; |
| | | return; |
| | | } |
| | | String result = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f", |
| | | center[0], center[1], radiusPoint[0], radiusPoint[1]); |
| | | circle.centerX, circle.centerY, radiusPoint[0], radiusPoint[1]); |
| | | 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); |
| | |
| | | } |
| | | 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("editingObstacleName"); |
| | | updateDrawingStatus(); |
| | | } |
| | | |
| | |
| | | |
| | | private void updateSaveButtonState() { |
| | | if (saveButton != null) { |
| | | saveButton.setEnabled(isMeaningfulValue(formData.get("obstacleCoordinates"))); |
| | | boolean hasCoords = isMeaningfulValue(formData.get("obstacleCoordinates")); |
| | | boolean hasName = isMeaningfulValue(formData.get("obstacleName")); |
| | | boolean enabled = hasCoords && hasName; |
| | | saveButton.setEnabled(enabled); |
| | | if (enabled) { |
| | | saveButton.setBackground(PRIMARY_COLOR); |
| | | saveButton.setForeground(WHITE); |
| | | saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | } else { |
| | | saveButton.setBackground(MEDIUM_GRAY); |
| | | saveButton.setForeground(TEXT_COLOR); |
| | | saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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(); |
| | | String obstacleName = formData.get("obstacleName"); |
| | | String coordsValue = formData.get("obstacleCoordinates"); |
| | | String originalValue = formData.get("obstacleOriginalCoordinates"); |
| | | 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; |
| | | 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, coords, originalCoords)) { |
| | | JOptionPane.showMessageDialog(this, "写入障碍物配置失败,请重试", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | if (!Dikuai.updateField(landNumber, "obstacleCoordinates", coords)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (originalCoords != null) { |
| | | if (!Dikuai.updateField(landNumber, "obstacleOriginalCoordinates", originalCoords)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新障碍物原始坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | } else if (!Dikuai.updateField(landNumber, "obstacleOriginalCoordinates", "-1")) { |
| | | 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 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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 ? "成功" : "提示", |
| | |
| | | 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)); |
| | |
| | | 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; |