| | |
| | | import javax.swing.*; |
| | | import javax.swing.SwingUtilities; |
| | | import java.awt.*; |
| | | import java.awt.event.ActionEvent; |
| | | import java.awt.event.ActionListener; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | |
| | | import dikuai.Dikuai; |
| | | import lujing.Lunjingguihua; |
| | | import lujing.ObstaclePathPlanner; |
| | | import lujing.Qufenxingzhuang; |
| | | import lujing.AoxinglujingNoObstacle; |
| | | import lujing.YixinglujingNoObstacle; |
| | | import publicway.Fuzhibutton; |
| | | import lujing.AoxinglujingHaveObstacel; |
| | | import lujing.YixinglujingHaveObstacel; |
| | | import org.locationtech.jts.geom.Coordinate; |
| | | import gecaoji.Device; |
| | | import java.util.Locale; |
| | | |
| | | /** |
| | | * 生成割草路径页面 |
| | |
| | | widthField = createInfoTextField(widthValue != null ? widthValue : "", true); |
| | | contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField)); |
| | | |
| | | // 割草安全距离(只读显示) |
| | | String displaySafetyDistance = "未设置"; |
| | | Device device = Device.getActiveDevice(); |
| | | if (device != null) { |
| | | String safetyDistanceValue = device.getMowingSafetyDistance(); |
| | | if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) { |
| | | try { |
| | | double distanceMeters = Double.parseDouble(safetyDistanceValue.trim()); |
| | | // 如果值大于100,认为是厘米,需要转换为米 |
| | | if (distanceMeters > 100) { |
| | | distanceMeters = distanceMeters / 100.0; |
| | | } |
| | | displaySafetyDistance = String.format("%.2f米", distanceMeters); |
| | | } catch (NumberFormatException e) { |
| | | displaySafetyDistance = "未设置"; |
| | | } |
| | | } |
| | | } |
| | | contentPanel.add(createInfoValueSection("割草安全距离", displaySafetyDistance)); |
| | | |
| | | // 割草模式(只读显示) |
| | | contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue))); |
| | | |
| | |
| | | obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' '); |
| | | } |
| | | |
| | | // 获取安全距离 |
| | | String safetyMarginStr = getSafetyDistanceString(); |
| | | if (safetyMarginStr == null) { |
| | | // 如果没有设置安全距离,使用默认值:割草宽度的一半 + 0.2米 |
| | | double defaultSafetyDistance = widthMeters / 2.0 + 0.2; |
| | | safetyMarginStr = BigDecimal.valueOf(defaultSafetyDistance) |
| | | .setScale(3, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | } |
| | | |
| | | String mode = normalizeExistingMowingPattern(modeInput); |
| | | try { |
| | | // 1. 首先判断地块类型(凸形还是异形) |
| | | Qufenxingzhuang shapeJudger = new Qufenxingzhuang(); |
| | | int grassType = shapeJudger.judgeGrassType(boundary); |
| | | // grassType: 0=无法判断, 1=凸形, 2=异形 |
| | | |
| | | // 解析障碍物列表 |
| | | List<List<Coordinate>> obstacleList = Lunjingguihua.parseObstacles(obstacles); |
| | | if (obstacleList == null) { |
| | |
| | | // 判断是否有有效的障碍物:只有当解析成功且列表不为空时,才认为有障碍物 |
| | | boolean hasValidObstacles = !obstacleList.isEmpty(); |
| | | |
| | | String generated; |
| | | String generated = null; |
| | | |
| | | // 2. 根据地块类型和是否有障碍物,调用不同的路径生成类 |
| | | if (!hasValidObstacles) { |
| | | // 障碍物坐标不存在或为空时,使用Lunjingguihua类的方法生成路径 |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, |
| | | obstacles != null ? obstacles : "", |
| | | plannerWidth, |
| | | null, // safetyDistStr,使用默认值 |
| | | mode |
| | | ); |
| | | } else { |
| | | // 有有效障碍物时,使用ObstaclePathPlanner处理路径生成 |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", |
| | | "错误", JOptionPane.ERROR_MESSAGE); |
| | | // 无障碍物的情况 |
| | | if (grassType == 1) { |
| | | // 凸形地块,无障碍物 -> 调用 AoxinglujingNoObstacle |
| | | List<AoxinglujingNoObstacle.PathSegment> segments = |
| | | AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | generated = formatAoxingPathSegments(segments); |
| | | } else if (grassType == 2) { |
| | | // 异形地块,无障碍物 -> 调用 YixinglujingNoObstacle |
| | | // 注意:如果该类还没有实现,这里会抛出异常或返回null |
| | | try { |
| | | // 调用 YixinglujingNoObstacle.planPath 获取路径段列表 |
| | | List<YixinglujingNoObstacle.PathSegment> segments = |
| | | YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | // 格式化路径段列表为字符串 |
| | | generated = formatYixingPathSegments(segments); |
| | | } catch (Exception e) { |
| | | // 如果类还没有实现,使用原来的方法作为后备 |
| | | if (showMessages) { |
| | | System.err.println("YixinglujingNoObstacle 尚未实现,使用默认方法: " + e.getMessage()); |
| | | } |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode); |
| | | } |
| | | return null; |
| | | } else { |
| | | // 无法判断地块类型,使用原来的方法作为后备 |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法", |
| | | "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode); |
| | | } |
| | | |
| | | // 有障碍物时使用割草宽度的一半 + 0.05米额外安全距离 |
| | | double safetyDistance = widthMeters / 2.0 + 0.05; |
| | | |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | | List<Lunjingguihua.PathSegment> segments = pathPlanner.generate(); |
| | | generated = Lunjingguihua.formatPathSegments(segments); |
| | | } else { |
| | | // 有障碍物的情况 |
| | | if (grassType == 1) { |
| | | // 凸形地块,有障碍物 -> 调用 AoxinglujingHaveObstacel |
| | | try { |
| | | // 假设 AoxinglujingHaveObstacel 有类似的方法签名 |
| | | generated = AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); |
| | | } catch (Exception e) { |
| | | // 如果类还没有实现,使用原来的方法作为后备 |
| | | if (showMessages) { |
| | | System.err.println("AoxinglujingHaveObstacel 尚未实现,使用默认方法: " + e.getMessage()); |
| | | } |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", |
| | | "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | double safetyDistance = Double.parseDouble(safetyMarginStr); |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | | List<Lunjingguihua.PathSegment> segments = pathPlanner.generate(); |
| | | generated = Lunjingguihua.formatPathSegments(segments); |
| | | } |
| | | } else if (grassType == 2) { |
| | | // 异形地块,有障碍物 -> 调用 YixinglujingHaveObstacel |
| | | try { |
| | | // 假设 YixinglujingHaveObstacel 有类似的方法签名 |
| | | generated = YixinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); |
| | | } catch (Exception e) { |
| | | // 如果类还没有实现,使用原来的方法作为后备 |
| | | if (showMessages) { |
| | | System.err.println("YixinglujingHaveObstacel 尚未实现,使用默认方法: " + e.getMessage()); |
| | | } |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", |
| | | "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | double safetyDistance = Double.parseDouble(safetyMarginStr); |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | | List<Lunjingguihua.PathSegment> segments = pathPlanner.generate(); |
| | | generated = Lunjingguihua.formatPathSegments(segments); |
| | | } |
| | | } else { |
| | | // 无法判断地块类型,使用原来的方法作为后备 |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法", |
| | | "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点", |
| | | "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | double safetyDistance = Double.parseDouble(safetyMarginStr); |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | | List<Lunjingguihua.PathSegment> segments = pathPlanner.generate(); |
| | | generated = Lunjingguihua.formatPathSegments(segments); |
| | | } |
| | | } |
| | | |
| | | String trimmed = generated != null ? generated.trim() : ""; |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 获取安全距离字符串(米) |
| | | */ |
| | | private String getSafetyDistanceString() { |
| | | Device device = Device.getActiveDevice(); |
| | | if (device != null) { |
| | | String safetyDistanceValue = device.getMowingSafetyDistance(); |
| | | if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) { |
| | | try { |
| | | double distanceMeters = Double.parseDouble(safetyDistanceValue.trim()); |
| | | // 如果值大于100,认为是厘米,需要转换为米 |
| | | if (distanceMeters > 100) { |
| | | distanceMeters = distanceMeters / 100.0; |
| | | } |
| | | return BigDecimal.valueOf(distanceMeters) |
| | | .setScale(3, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | } catch (NumberFormatException e) { |
| | | // 解析失败,返回null,使用默认值 |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 格式化 AoxinglujingNoObstacle.PathSegment 列表为坐标字符串 |
| | | */ |
| | | private String formatAoxingPathSegments(List<AoxinglujingNoObstacle.PathSegment> segments) { |
| | | if (segments == null || segments.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | AoxinglujingNoObstacle.Point last = null; |
| | | for (AoxinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equals2D(last, segment.start)) { |
| | | appendPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendPoint(sb, segment.end); |
| | | last = segment.end; |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 格式化 YixinglujingNoObstacle.PathSegment 列表为坐标字符串 |
| | | */ |
| | | private String formatYixingPathSegments(List<YixinglujingNoObstacle.PathSegment> segments) { |
| | | if (segments == null || segments.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | YixinglujingNoObstacle.Point last = null; |
| | | for (YixinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equalsYixingPoint(last, segment.start)) { |
| | | appendYixingPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendYixingPoint(sb, segment.end); |
| | | last = segment.end; |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 比较两个点是否相同(使用小的容差) |
| | | */ |
| | | private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.Point p2) { |
| | | if (p1 == null || p2 == null) { |
| | | return p1 == p2; |
| | | } |
| | | double tolerance = 1e-6; |
| | | return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; |
| | | } |
| | | |
| | | /** |
| | | * 添加点到字符串构建器 |
| | | */ |
| | | private void appendPoint(StringBuilder sb, AoxinglujingNoObstacle.Point point) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y)); |
| | | } |
| | | |
| | | /** |
| | | * 比较两个 YixinglujingNoObstacle.Point 是否相同(使用小的容差) |
| | | */ |
| | | private boolean equalsYixingPoint(YixinglujingNoObstacle.Point p1, YixinglujingNoObstacle.Point p2) { |
| | | if (p1 == null || p2 == null) { |
| | | return p1 == p2; |
| | | } |
| | | double tolerance = 1e-6; |
| | | return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; |
| | | } |
| | | |
| | | /** |
| | | * 添加 YixinglujingNoObstacle.Point 到字符串构建器 |
| | | */ |
| | | private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y)); |
| | | } |
| | | |
| | | // ========== UI辅助方法 ========== |
| | | |
| | | private JTextArea createInfoTextArea(String text, boolean editable, int rows) { |
| | |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | // 创建标题面板,包含标题和复制图标 |
| | | JPanel titlePanel = new JPanel(new BorderLayout()); |
| | | titlePanel.setBackground(BACKGROUND_COLOR); |
| | | titlePanel.setOpaque(false); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | titlePanel.add(titleLabel, BorderLayout.WEST); |
| | | |
| | | // 创建复制按钮(使用 Fuzhibutton) |
| | | JButton copyButton = Fuzhibutton.createCopyButton( |
| | | () -> { |
| | | String text = textArea.getText(); |
| | | if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) { |
| | | return null; // 返回null会触发"未设置"提示 |
| | | } |
| | | return text; |
| | | }, |
| | | "复制" + title, |
| | | new Color(230, 250, 240) |
| | | ); |
| | | titlePanel.add(copyButton, BorderLayout.EAST); |
| | | |
| | | section.add(titlePanel, BorderLayout.NORTH); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | // 创建标题面板,包含标题和复制图标 |
| | | JPanel titlePanel = new JPanel(new BorderLayout()); |
| | | titlePanel.setBackground(BACKGROUND_COLOR); |
| | | titlePanel.setOpaque(false); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | titlePanel.add(titleLabel, BorderLayout.WEST); |
| | | |
| | | // 创建复制按钮(使用 Fuzhibutton) |
| | | JButton copyButton = Fuzhibutton.createCopyButton( |
| | | () -> { |
| | | String text = textField.getText(); |
| | | if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) { |
| | | return null; // 返回null会触发"未设置"提示 |
| | | } |
| | | return text; |
| | | }, |
| | | "复制" + title, |
| | | new Color(230, 250, 240) |
| | | ); |
| | | titlePanel.add(copyButton, BorderLayout.EAST); |
| | | |
| | | section.add(titlePanel, BorderLayout.NORTH); |
| | | |
| | | JPanel fieldWrapper = new JPanel(new BorderLayout()); |
| | | fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245)); |
| | |
| | | } |
| | | return "parallel"; |
| | | } |
| | | |
| | | } |
| | | |
| | | |