| | |
| | | import java.awt.datatransfer.*; |
| | | import java.awt.datatransfer.StringSelection; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.FileOutputStream; |
| | | import java.io.IOException; |
| | | import java.math.BigDecimal; |
| | | import ui.UIConfig; |
| | | import ui.UIUtils; |
| | | import java.math.RoundingMode; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.Map; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Objects; |
| | | import java.util.Properties; |
| | | |
| | | import lujing.Lunjingguihua; |
| | | import lujing.MowingPathGenerationPage; |
| | | import publicway.Fuzhibutton; |
| | | import publicway.Lookbutton; |
| | | import publicway.buttonset; |
| | | import zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhuye.MapRenderer; |
| | | import zhuye.Shouye; |
| | | import zhuye.Coordinate; |
| | | import gecaoji.Device; |
| | | |
| | | /** |
| | | * 地块管理面板 - 卡片式布局设计 |
| | |
| | | private JButton addLandBtn; |
| | | |
| | | private static String currentWorkLandNumber; |
| | | private static final String WORK_LAND_KEY = "currentWorkLandNumber"; |
| | | private static final String PROPERTIES_FILE = "set.properties"; |
| | | private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>(); |
| | | private ImageIcon workSelectedIcon; |
| | | private ImageIcon workUnselectedIcon; |
| | | private ImageIcon boundaryVisibleIcon; |
| | | private ImageIcon boundaryHiddenIcon; |
| | | private static final int BOUNDARY_TOGGLE_ICON_SIZE = 48; |
| | | private static final int BOUNDARY_TOGGLE_ICON_SIZE = 24; |
| | | private Map<String, ObstacleSummary> obstacleSummaryCache = Collections.emptyMap(); |
| | | |
| | | public Dikuaiguanli(String landNumber) { |
| | | latestInstance = this; |
| | |
| | | cardsPanel.removeAll(); |
| | | |
| | | Map<String, Dikuai> allDikuai = Dikuai.getAllDikuai(); |
| | | |
| | | |
| | | if (allDikuai.isEmpty()) { |
| | | obstacleSummaryCache = Collections.emptyMap(); |
| | | // 显示空状态 |
| | | JPanel emptyPanel = createEmptyStatePanel(); |
| | | cardsPanel.add(emptyPanel); |
| | | setCurrentWorkLand(null, null); |
| | | } else { |
| | | obstacleSummaryCache = loadObstacleSummaries(); |
| | | if (allDikuai.size() == 1) { |
| | | Dikuai onlyDikuai = allDikuai.values().iterator().next(); |
| | | setCurrentWorklandIfNeeded(onlyDikuai); |
| | |
| | | for (Dikuai dikuai : allDikuai.values()) { |
| | | JPanel card = createDikuaiCard(dikuai); |
| | | cardsPanel.add(card); |
| | | cardsPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | cardsPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | headerPanel.add(nameLabel, BorderLayout.WEST); |
| | | |
| | | // 右侧区域:状态文字 + 按钮 |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | rightPanel.setOpaque(false); |
| | | |
| | | // 状态文字标签(根据是否选中显示/隐藏) |
| | | JLabel statusLabel = new JLabel("已设置为当前地块"); |
| | | statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | statusLabel.setForeground(PRIMARY_COLOR); |
| | | boolean isCurrent = dikuai.getLandNumber() != null && dikuai.getLandNumber().equals(currentWorkLandNumber); |
| | | statusLabel.setVisible(isCurrent); |
| | | |
| | | JButton workToggleBtn = createWorkToggleButton(dikuai); |
| | | headerPanel.add(workToggleBtn, BorderLayout.EAST); |
| | | |
| | | // 将状态标签和按钮关联,以便在按钮状态变化时更新标签 |
| | | workToggleBtn.putClientProperty("statusLabel", statusLabel); |
| | | |
| | | rightPanel.add(statusLabel); |
| | | rightPanel.add(workToggleBtn); |
| | | |
| | | headerPanel.add(rightPanel, BorderLayout.EAST); |
| | | |
| | | card.add(headerPanel, BorderLayout.NORTH); |
| | | |
| | |
| | | |
| | | // 地块编号 |
| | | contentPanel.add(createCardInfoItem("地块编号:", getDisplayValue(dikuai.getLandNumber(), "未知"))); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 添加时间 |
| | | contentPanel.add(createCardInfoItem("添加时间:", getDisplayValue(dikuai.getCreateTime(), "未知"))); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 地块面积 |
| | | String landArea = dikuai.getLandArea(); |
| | | if (landArea != null && !landArea.equals("-1")) { |
| | |
| | | landArea = "未知"; |
| | | } |
| | | contentPanel.add(createCardInfoItem("地块面积:", landArea)); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | // 返回点坐标(带修改按钮) |
| | | contentPanel.add(createCardInfoItemWithButton("返回点坐标:", |
| | | getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"), |
| | | "修改", e -> editReturnPoint(dikuai))); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | // 地块边界坐标(带显示顶点按钮) |
| | | JPanel boundaryPanel = createBoundaryInfoItem(dikuai, |
| | | getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "未设置")); |
| | | setInfoItemTooltip(boundaryPanel, dikuai.getBoundaryCoordinates()); |
| | | contentPanel.add(boundaryPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | // 障碍物坐标(带新增和查看按钮) |
| | | JPanel obstaclePanel = createCardInfoItemWithButton("障碍物坐标:", |
| | | getTruncatedValue(dikuai.getObstacleCoordinates(), 12, "未设置"), |
| | | "新增", |
| | | e -> addNewObstacle(dikuai)); |
| | | setInfoItemTooltip(obstaclePanel, dikuai.getObstacleCoordinates()); |
| | | contentPanel.add(obstaclePanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 路径坐标(带查看按钮) |
| | | JPanel pathPanel = createCardInfoItemWithButton("路径坐标:", |
| | | getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"), |
| | | "复制", e -> copyCoordinatesAction("路径坐标", dikuai.getPlannedPath())); |
| | | setInfoItemTooltip(pathPanel, dikuai.getPlannedPath()); |
| | | contentPanel.add(pathPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | JPanel baseStationPanel = createCardInfoItemWithButton("基站坐标:", |
| | | getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "未设置"), |
| | | "复制", e -> copyCoordinatesAction("基站坐标", dikuai.getBaseStationCoordinates())); |
| | | setInfoItemTooltip(baseStationPanel, dikuai.getBaseStationCoordinates()); |
| | | contentPanel.add(baseStationPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | JPanel boundaryOriginalPanel = createCardInfoItemWithButton("边界原始坐标:", |
| | | getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "未设置"), |
| | | "复制", e -> copyCoordinatesAction("边界原始坐标", dikuai.getBoundaryOriginalCoordinates())); |
| | | setInfoItemTooltip(boundaryOriginalPanel, dikuai.getBoundaryOriginalCoordinates()); |
| | | contentPanel.add(boundaryOriginalPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | |
| | | JPanel mowingPatternPanel = createCardInfoItemWithButton("割草模式:", |
| | | getTruncatedValue(dikuai.getMowingPattern(), 12, "未设置"), |
| | | "复制", e -> copyCoordinatesAction("割草模式", dikuai.getMowingPattern())); |
| | | setInfoItemTooltip(mowingPatternPanel, dikuai.getMowingPattern()); |
| | | JPanel mowingPatternPanel = createCardInfoItem("割草模式:", |
| | | formatMowingPatternForDisplay(dikuai.getMowingPattern())); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(mowingPatternPanel), |
| | | () -> editMowingPattern(dikuai), |
| | | "点击查看/编辑割草模式"); |
| | | contentPanel.add(mowingPatternPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 割草机割刀宽度 |
| | | String mowingBladeWidthValue = dikuai.getMowingBladeWidth(); |
| | | String displayBladeWidth = "未设置"; |
| | | if (mowingBladeWidthValue != null && !"-1".equals(mowingBladeWidthValue) && !mowingBladeWidthValue.trim().isEmpty()) { |
| | | try { |
| | | double bladeWidthMeters = Double.parseDouble(mowingBladeWidthValue.trim()); |
| | | double bladeWidthCm = bladeWidthMeters * 100.0; |
| | | displayBladeWidth = String.format("%.2f厘米", bladeWidthCm); |
| | | } catch (NumberFormatException e) { |
| | | displayBladeWidth = "未设置"; |
| | | } |
| | | } |
| | | JPanel mowingBladeWidthPanel = createCardInfoItem("割草机割刀宽度:", displayBladeWidth); |
| | | contentPanel.add(mowingBladeWidthPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | String mowingWidthValue = dikuai.getMowingWidth(); |
| | | String widthSource = null; |
| | | String displayWidth = "未设置"; |
| | | if (mowingWidthValue != null && !"-1".equals(mowingWidthValue) && !mowingWidthValue.trim().isEmpty()) { |
| | | widthSource = mowingWidthValue + "厘米"; |
| | | displayWidth = mowingWidthValue + "厘米"; |
| | | } |
| | | String displayWidth = getTruncatedValue(widthSource, 12, "未设置"); |
| | | JPanel mowingWidthPanel = createCardInfoItemWithButton("割草宽度:", |
| | | displayWidth, |
| | | "编辑", e -> editMowingWidth(dikuai)); |
| | | setInfoItemTooltip(mowingWidthPanel, widthSource); |
| | | JPanel mowingWidthPanel = createCardInfoItem("割草宽度:", displayWidth); |
| | | contentPanel.add(mowingWidthPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 割草安全距离 |
| | | String displaySafetyDistance = "未设置"; |
| | | String safetyDistanceValue = dikuai.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 = "未设置"; |
| | | } |
| | | } |
| | | JPanel mowingSafetyDistancePanel = createCardInfoItem("割草安全距离:", displaySafetyDistance); |
| | | contentPanel.add(mowingSafetyDistancePanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 往返点路径(带查看图标按钮) |
| | | JPanel returnPathPanel = createCardInfoItemWithIconButton("往返点路径:", |
| | | createViewButton(e -> editReturnPath(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(returnPathPanel), |
| | | () -> editReturnPath(dikuai), |
| | | "点击查看/编辑往返点路径"); |
| | | contentPanel.add(returnPathPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber()); |
| | | JPanel obstaclePanel = createCardInfoItemWithButton("障碍物:", |
| | | obstacleSummary.buildDisplayValue(), |
| | | "新增", |
| | | e -> addNewObstacle(dikuai)); |
| | | setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip()); |
| | | // 让障碍物标题可点击,打开障碍物管理页面 |
| | | configureInteractiveLabel(getInfoItemTitleLabel(obstaclePanel), |
| | | () -> showObstacleManagementPage(dikuai), |
| | | "点击查看/管理障碍物"); |
| | | contentPanel.add(obstaclePanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 地块边界坐标(带查看按钮) |
| | | JPanel boundaryPanel = createCardInfoItemWithIconButton("地块边界:", |
| | | createViewButton(e -> editBoundaryCoordinates(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel), |
| | | () -> editBoundaryCoordinates(dikuai), |
| | | "点击查看/编辑地块边界坐标"); |
| | | contentPanel.add(boundaryPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // 路径坐标(带查看按钮) |
| | | JPanel pathPanel = createCardInfoItemWithIconButton("路径坐标:", |
| | | createViewButton(e -> editPlannedPath(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(pathPanel), |
| | | () -> editPlannedPath(dikuai), |
| | | "点击查看/编辑路径坐标"); |
| | | contentPanel.add(pathPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | JPanel baseStationPanel = createCardInfoItemWithIconButton("基站坐标:", |
| | | createViewButton(e -> editBaseStationCoordinates(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(baseStationPanel), |
| | | () -> editBaseStationCoordinates(dikuai), |
| | | "点击查看/编辑基站坐标"); |
| | | contentPanel.add(baseStationPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:", |
| | | getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"), |
| | | createViewButton(e -> showCompletedMowingTrackDialog(dikuai))); |
| | | setInfoItemTooltip(completedTrackPanel, dikuai.getMowingTrack()); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(completedTrackPanel), |
| | | () -> showCompletedMowingTrackDialog(dikuai), |
| | | "点击查看完成的割草路径记录"); |
| | | contentPanel.add(completedTrackPanel); |
| | | |
| | | card.add(contentPanel, BorderLayout.CENTER); |
| | | |
| | | JButton deleteBtn = createDeleteButton(); |
| | | deleteBtn.addActionListener(e -> deleteDikuai(dikuai)); |
| | | |
| | | JButton generatePathBtn = createPrimaryFooterButton("路径规划"); |
| | | generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai)); |
| | | |
| | | JButton navigationPreviewBtn = createPrimaryFooterButton("导航预览"); |
| | | navigationPreviewBtn.addActionListener(e -> startNavigationPreview(dikuai)); |
| | | |
| | | JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); |
| | | footerPanel.setBackground(CARD_BACKGROUND); |
| | | footerPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); |
| | | footerPanel.add(generatePathBtn); |
| | | footerPanel.add(Box.createHorizontalStrut(12)); |
| | | footerPanel.add(navigationPreviewBtn); |
| | | footerPanel.add(Box.createHorizontalStrut(12)); |
| | | footerPanel.add(deleteBtn); |
| | | card.add(footerPanel, BorderLayout.SOUTH); |
| | | |
| | |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(valueComp, BorderLayout.EAST); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | |
| | | private JPanel createCardInfoItemWithButton(String label, String value, String buttonText, ActionListener listener) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); |
| | | // 增加高度以确保按钮完整显示(按钮高度约24-28像素,加上上下边距) |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); |
| | | itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel(label); |
| | | labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | // 添加垂直内边距以确保按钮不被裁剪 |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); |
| | | |
| | | JLabel valueComp = new JLabel(value); |
| | | valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | valueComp.setForeground(TEXT_COLOR); |
| | | |
| | | JButton button = createSmallButton(buttonText); |
| | | button.addActionListener(listener); |
| | | JButton button = createSmallLinkButton(buttonText, listener); |
| | | |
| | | rightPanel.add(valueComp); |
| | | rightPanel.add(button); |
| | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createBoundaryInfoItem(Dikuai dikuai, String displayValue) { |
| | | private JPanel createCardInfoItemWithButton(String label, String value, JButton button) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | // 增加高度以确保按钮完整显示(按钮高度约24-28像素,加上上下边距) |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); |
| | | itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel(label); |
| | | labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | labelComp.setForeground(LIGHT_TEXT); |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | // 添加垂直内边距以确保按钮不被裁剪 |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); |
| | | |
| | | JLabel valueComp = new JLabel(value); |
| | | valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | valueComp.setForeground(TEXT_COLOR); |
| | | |
| | | rightPanel.add(valueComp); |
| | | rightPanel.add(button); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createCardInfoItemWithButtonOnly(String label, String buttonText, ActionListener listener) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | // 增加高度以确保按钮完整显示(按钮高度约24-28像素,加上上下边距) |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); |
| | | itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel(label); |
| | | labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | labelComp.setForeground(LIGHT_TEXT); |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | // 添加垂直内边距以确保按钮不被裁剪 |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); |
| | | |
| | | JButton button = createSmallLinkButton(buttonText, listener); |
| | | |
| | | rightPanel.add(button); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createCardInfoItemWithIconButton(String label, JButton button) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | // 增加高度以确保按钮完整显示(按钮高度约24-28像素,加上上下边距) |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); |
| | | itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel(label); |
| | | labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | labelComp.setForeground(LIGHT_TEXT); |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | // 添加垂直内边距以确保按钮不被裁剪 |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); |
| | | |
| | | rightPanel.add(button); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createBoundaryInfoItem(Dikuai dikuai) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | int rowHeight = Math.max(36, BOUNDARY_TOGGLE_ICON_SIZE + 12); |
| | | // 增加高度以确保按钮下边缘完整显示(按钮高度28,加上上下边距) |
| | | int rowHeight = Math.max(30, BOUNDARY_TOGGLE_ICON_SIZE + 8); |
| | | Dimension rowDimension = new Dimension(Integer.MAX_VALUE, rowHeight); |
| | | itemPanel.setMaximumSize(rowDimension); |
| | | itemPanel.setPreferredSize(rowDimension); |
| | | itemPanel.setMinimumSize(new Dimension(0, 32)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel("地块边界:"); |
| | | labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | labelComp.setForeground(LIGHT_TEXT); |
| | | |
| | | int verticalPadding = Math.max(0, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2); |
| | | // 确保按钮有足够的上下边距,避免下边缘被裁剪 |
| | | int verticalPadding = Math.max(2, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2); |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(verticalPadding, 0, verticalPadding, 0)); |
| | | |
| | | JLabel valueComp = new JLabel(displayValue); |
| | | valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | valueComp.setForeground(TEXT_COLOR); |
| | | // 状态提示文字标签 |
| | | JLabel statusLabel = new JLabel(); |
| | | statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | statusLabel.setForeground(LIGHT_TEXT); |
| | | |
| | | JButton toggleButton = createBoundaryToggleButton(dikuai); |
| | | // 将状态标签和按钮关联,以便在按钮状态变化时更新标签 |
| | | toggleButton.putClientProperty("statusLabel", statusLabel); |
| | | |
| | | rightPanel.add(valueComp); |
| | | // 初始化状态文字 |
| | | String landNumber = dikuai.getLandNumber(); |
| | | boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false); |
| | | updateBoundaryStatusLabel(statusLabel, isVisible); |
| | | |
| | | rightPanel.add(statusLabel); |
| | | rightPanel.add(toggleButton); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.setMargin(new Insets(0, 0, 0, 0)); |
| | | button.setIconTextGap(0); |
| | | button.setPreferredSize(new Dimension(56, 56)); |
| | | button.setPreferredSize(new Dimension(28, 28)); |
| | | |
| | | String landNumber = dikuai.getLandNumber(); |
| | | boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false); |
| | |
| | | button.setOpaque(true); |
| | | } |
| | | button.setToolTipText(active ? "隐藏边界点序号" : "显示边界点序号"); |
| | | |
| | | // 更新状态提示文字 |
| | | Object statusLabelObj = button.getClientProperty("statusLabel"); |
| | | if (statusLabelObj instanceof JLabel) { |
| | | JLabel statusLabel = (JLabel) statusLabelObj; |
| | | updateBoundaryStatusLabel(statusLabel, active); |
| | | } |
| | | } |
| | | |
| | | private void updateBoundaryStatusLabel(JLabel statusLabel, boolean active) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if (active) { |
| | | statusLabel.setText("已开启边界点显示"); |
| | | } else { |
| | | statusLabel.setText("已关闭边界点显示"); |
| | | } |
| | | } |
| | | |
| | | private void ensureBoundaryToggleIconsLoaded() { |
| | |
| | | boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber); |
| | | if (isCurrent) { |
| | | renderer.setBoundaryPointsVisible(desiredState); |
| | | renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d); |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private JLabel getInfoItemTitleLabel(JPanel itemPanel) { |
| | | if (itemPanel == null) { |
| | | return null; |
| | | } |
| | | Object titleComp = itemPanel.getClientProperty("titleLabel"); |
| | | return titleComp instanceof JLabel ? (JLabel) titleComp : null; |
| | | } |
| | | |
| | | private void configureInteractiveLabel(JLabel label, Runnable onClick, String tooltip) { |
| | | if (label == null || onClick == null) { |
| | | return; |
| | | } |
| | | Color originalColor = label.getForeground(); |
| | | label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | if (tooltip != null && !tooltip.trim().isEmpty()) { |
| | | label.setToolTipText(tooltip); |
| | | } |
| | | label.addMouseListener(new MouseAdapter() { |
| | | public void mouseClicked(MouseEvent e) { |
| | | if (SwingUtilities.isLeftMouseButton(e)) { |
| | | onClick.run(); |
| | | } |
| | | } |
| | | |
| | | public void mouseEntered(MouseEvent e) { |
| | | label.setForeground(PRIMARY_COLOR); |
| | | } |
| | | |
| | | public void mouseExited(MouseEvent e) { |
| | | label.setForeground(originalColor); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private String prepareCoordinateForEditor(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return ""; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private String normalizeCoordinateInput(String input) { |
| | | if (input == null) { |
| | | return "-1"; |
| | | } |
| | | String trimmed = input.trim(); |
| | | return trimmed.isEmpty() ? "-1" : trimmed; |
| | | } |
| | | |
| | | private String promptCoordinateEditing(String title, String initialValue) { |
| | | return promptCoordinateEditing(title, initialValue, null); |
| | | } |
| | | |
| | | private String promptCoordinateEditing(String title, String initialValue, Dikuai dikuai) { |
| | | // 判断是否是往返点路径对话框 |
| | | boolean isReturnPathDialog = title != null && title.contains("往返点路径"); |
| | | |
| | | if (isReturnPathDialog) { |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | Wangfanpathpage page = new Wangfanpathpage(owner, title, initialValue, dikuai); |
| | | page.setVisible(true); |
| | | return page.getResult(); |
| | | } |
| | | |
| | | JTextArea textArea = new JTextArea(prepareCoordinateForEditor(initialValue)); |
| | | textArea.setLineWrap(true); |
| | | textArea.setWrapStyleWord(true); |
| | | textArea.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | textArea.setCaretPosition(0); |
| | | textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | // 如果是往返点路径对话框,高度调整为适应两个文本域 |
| | | scrollPane.setPreferredSize(new Dimension(360, isReturnPathDialog ? 100 : 240)); |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | JDialog dialog; |
| | | if (owner instanceof Frame) { |
| | | dialog = new JDialog((Frame) owner, title, true); |
| | | } else if (owner instanceof Dialog) { |
| | | dialog = new JDialog((Dialog) owner, title, true); |
| | | } else { |
| | | dialog = new JDialog((Frame) null, title, true); |
| | | } |
| | | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | |
| | | JPanel contentPanel = new JPanel(); |
| | | contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); |
| | | |
| | | if (isReturnPathDialog) { |
| | | contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); |
| | | // 减小边距以增加文本域宽度 (98%左右) |
| | | contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); |
| | | |
| | | // 1. 原始往返路径坐标区域 |
| | | String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getReturnPathRawCoordinates()) : ""; |
| | | int rawCount = 0; |
| | | if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) { |
| | | rawCount = rawCoords.split(";").length; |
| | | } |
| | | |
| | | JPanel rawHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); |
| | | rawHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | rawHeaderPanel.setBackground(BACKGROUND_COLOR); |
| | | rawHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | |
| | | JLabel rawTitleLabel = new JLabel("原始往返路径坐标 (" + rawCount + "点) "); |
| | | rawTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | rawHeaderPanel.add(rawTitleLabel); |
| | | |
| | | // 原始坐标复制按钮 |
| | | final String finalRawCoords = rawCoords; |
| | | JButton rawCopyBtn = Fuzhibutton.createCopyButton( |
| | | () -> { |
| | | if (finalRawCoords == null || finalRawCoords.isEmpty() || "-1".equals(finalRawCoords)) return null; |
| | | return finalRawCoords; |
| | | }, |
| | | "复制", |
| | | new Color(230, 250, 240) |
| | | ); |
| | | rawCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | rawCopyBtn.setPreferredSize(new Dimension(50, 24)); |
| | | rawCopyBtn.setMargin(new Insets(0,0,0,0)); |
| | | rawHeaderPanel.add(rawCopyBtn); |
| | | |
| | | contentPanel.add(rawHeaderPanel); |
| | | contentPanel.add(Box.createVerticalStrut(5)); |
| | | |
| | | JTextArea rawTextArea = new JTextArea(rawCoords); |
| | | rawTextArea.setLineWrap(true); |
| | | rawTextArea.setWrapStyleWord(true); |
| | | rawTextArea.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | rawTextArea.setEditable(false); // 原始坐标通常不可编辑 |
| | | rawTextArea.setRows(4); |
| | | rawTextArea.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); |
| | | |
| | | JScrollPane rawScroll = new JScrollPane(rawTextArea); |
| | | rawScroll.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | // 设置最大宽度允许扩展,首选宽度适中 |
| | | rawScroll.setPreferredSize(new Dimension(300, 100)); |
| | | rawScroll.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100)); |
| | | contentPanel.add(rawScroll); |
| | | |
| | | contentPanel.add(Box.createVerticalStrut(15)); |
| | | |
| | | // 2. 优化后往返路径坐标区域 |
| | | String optCoords = prepareCoordinateForEditor(initialValue); |
| | | int optCount = 0; |
| | | if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) { |
| | | optCount = optCoords.split(";").length; |
| | | } |
| | | |
| | | JPanel optHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); |
| | | optHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | optHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | |
| | | JLabel optTitleLabel = new JLabel("优化后往返路径坐标 (" + optCount + "点) "); |
| | | optTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | optHeaderPanel.add(optTitleLabel); |
| | | |
| | | // 优化坐标复制按钮 - 动态获取文本域内容 |
| | | JButton optCopyBtn = Fuzhibutton.createCopyButton( |
| | | () -> { |
| | | String text = textArea.getText(); |
| | | if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) return null; |
| | | return text; |
| | | }, |
| | | "复制", |
| | | new Color(230, 250, 240) |
| | | ); |
| | | optCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | optCopyBtn.setPreferredSize(new Dimension(50, 24)); |
| | | optCopyBtn.setMargin(new Insets(0,0,0,0)); |
| | | optHeaderPanel.add(optCopyBtn); |
| | | |
| | | contentPanel.add(optHeaderPanel); |
| | | contentPanel.add(Box.createVerticalStrut(5)); |
| | | |
| | | // 使用传入的 textArea (已初始化为 initialValue) |
| | | textArea.setRows(4); |
| | | scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | scrollPane.setPreferredSize(new Dimension(300, 100)); |
| | | scrollPane.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100)); |
| | | contentPanel.add(scrollPane); |
| | | |
| | | } else { |
| | | contentPanel.setLayout(new BorderLayout()); |
| | | contentPanel.add(scrollPane, BorderLayout.CENTER); |
| | | } |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); |
| | | |
| | | JButton okButton; |
| | | JButton cancelButton; |
| | | JButton copyButton = null; // 初始化为null |
| | | |
| | | if (isReturnPathDialog) { |
| | | // 往返点路径对话框:使用 buttonset 风格的确定按钮,图标按钮 |
| | | okButton = buttonset.createStyledButton("去绘制", new Color(70, 130, 220)); |
| | | |
| | | // 取消按钮使用 closepage.png 图标 |
| | | cancelButton = new JButton(); |
| | | ImageIcon closeIcon = loadIcon("image/closepage.png", 25, 25); |
| | | if (closeIcon != null) { |
| | | cancelButton.setIcon(closeIcon); |
| | | } else { |
| | | cancelButton.setText("关闭"); |
| | | } |
| | | cancelButton.setFont(new Font("微软雅黑", Font.PLAIN, 11)); |
| | | cancelButton.setForeground(PRIMARY_COLOR); |
| | | cancelButton.setBorder(BorderFactory.createEmptyBorder()); |
| | | cancelButton.setContentAreaFilled(false); |
| | | cancelButton.setFocusPainted(false); |
| | | cancelButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | cancelButton.addMouseListener(new MouseAdapter() { |
| | | public void mouseEntered(MouseEvent e) { cancelButton.setOpaque(true); cancelButton.setBackground(new Color(255, 240, 240)); } |
| | | public void mouseExited(MouseEvent e) { cancelButton.setOpaque(false); } |
| | | }); |
| | | |
| | | // 使用 Fuzhibutton 创建复制按钮 (这里不再需要底部的复制按钮,因为上面已经有了) |
| | | // copyButton = ... |
| | | |
| | | } else { |
| | | // 其他对话框保持原有样式 |
| | | okButton = new JButton("确定"); |
| | | cancelButton = new JButton("取消"); |
| | | copyButton = new JButton("复制"); |
| | | |
| | | // 其他对话框的复制按钮逻辑 |
| | | copyButton.addActionListener(e -> { |
| | | String text = textArea.getText(); |
| | | if (text == null) { |
| | | text = ""; |
| | | } |
| | | String trimmed = text.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | JOptionPane.showMessageDialog(dialog, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | try { |
| | | StringSelection selection = new StringSelection(text); |
| | | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); |
| | | clipboard.setContents(selection, selection); |
| | | JOptionPane.showMessageDialog(dialog, title + " 已复制到剪贴板", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | } catch (Exception ex) { |
| | | JOptionPane.showMessageDialog(dialog, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | final boolean[] confirmed = new boolean[] {false}; |
| | | final String[] resultHolder = new String[1]; |
| | | |
| | | okButton.addActionListener(e -> { |
| | | if (isReturnPathDialog) { |
| | | // 往返点路径对话框:标记为打开绘制页面 |
| | | // 如果文本域中已经有坐标,表示要重新绘制 |
| | | String currentText = textArea.getText(); |
| | | if (currentText != null && !currentText.trim().isEmpty() && !"-1".equals(currentText.trim())) { |
| | | // 有坐标,表示重新绘制 |
| | | resultHolder[0] = "__OPEN_DRAW_PAGE_REFRESH__"; |
| | | } else { |
| | | // 没有坐标,正常绘制 |
| | | resultHolder[0] = "__OPEN_DRAW_PAGE__"; |
| | | } |
| | | confirmed[0] = true; |
| | | dialog.dispose(); |
| | | } else { |
| | | resultHolder[0] = textArea.getText(); |
| | | confirmed[0] = true; |
| | | dialog.dispose(); |
| | | } |
| | | }); |
| | | |
| | | cancelButton.addActionListener(e -> dialog.dispose()); |
| | | |
| | | buttonPanel.add(okButton); |
| | | buttonPanel.add(cancelButton); |
| | | if (copyButton != null) { |
| | | buttonPanel.add(copyButton); |
| | | } |
| | | |
| | | contentPanel.add(buttonPanel, isReturnPathDialog ? null : BorderLayout.SOUTH); |
| | | if (isReturnPathDialog) { |
| | | // 对于 BoxLayout,直接添加到底部 |
| | | JPanel bottomWrapper = new JPanel(new BorderLayout()); |
| | | bottomWrapper.add(buttonPanel, BorderLayout.EAST); |
| | | contentPanel.add(bottomWrapper); |
| | | } |
| | | dialog.setContentPane(contentPanel); |
| | | dialog.getRootPane().setDefaultButton(okButton); |
| | | dialog.pack(); |
| | | |
| | | // 如果是往返点路径对话框,设置宽度为首页的90%,高度保持不变 |
| | | if (isReturnPathDialog) { |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null && shouye.getWidth() > 0) { |
| | | int homeWidth = shouye.getWidth(); |
| | | int dialogWidth = (int)(homeWidth * 0.9); |
| | | Dimension currentSize = dialog.getSize(); |
| | | dialog.setSize(dialogWidth, currentSize.height); |
| | | } |
| | | } |
| | | |
| | | dialog.setLocationRelativeTo(this); |
| | | dialog.setVisible(true); |
| | | |
| | | return confirmed[0] ? resultHolder[0] : null; |
| | | } |
| | | |
| | | private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) { |
| | | if (dikuai == null || fieldName == null || dikuai.getLandNumber() == null) { |
| | | return false; |
| | | } |
| | | if (!Dikuai.updateField(dikuai.getLandNumber(), fieldName, value)) { |
| | | return false; |
| | | } |
| | | Dikuai.updateField(dikuai.getLandNumber(), "updateTime", getCurrentTime()); |
| | | Dikuai.saveToProperties(); |
| | | boolean isCurrent = dikuai.getLandNumber().equals(currentWorkLandNumber); |
| | | loadDikuaiData(); |
| | | if (isCurrent) { |
| | | setCurrentWorkLand(dikuai.getLandNumber(), dikuai.getLandName()); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private void editBoundaryCoordinates(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // 获取地块管理对话框,准备在打开边界编辑页面时关闭 |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // 打开边界编辑页面 |
| | | Dikuanbianjipage page = new Dikuanbianjipage(owner, "地块边界管理页面", dikuai.getBoundaryCoordinates(), dikuai); |
| | | page.setVisible(true); |
| | | |
| | | // 关闭地块管理页面 |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | // 获取编辑结果并保存 |
| | | String edited = page.getResult(); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", normalized)) { |
| | | JOptionPane.showMessageDialog(null, "无法更新地块边界坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "地块边界坐标已清空" : "地块边界坐标已更新"; |
| | | JOptionPane.showMessageDialog(null, message, "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void editPlannedPath(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String edited = promptCoordinateEditing("查看 / 编辑路径坐标", dikuai.getPlannedPath()); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "plannedPath", normalized)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新路径坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "路径坐标已清空" : "路径坐标已更新"; |
| | | JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void editBaseStationCoordinates(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String edited = promptCoordinateEditing("查看 / 编辑基站坐标", dikuai.getBaseStationCoordinates()); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", normalized)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新基站坐标", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "基站坐标已清空" : "基站坐标已更新"; |
| | | JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | |
| | | private void editMowingPattern(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String current = sanitizeValueOrNull(dikuai.getMowingPattern()); |
| | | String normalized = normalizeExistingMowingPattern(current); |
| | | JRadioButton parallelBtn = new JRadioButton("平行线 (parallel)"); |
| | | JRadioButton spiralBtn = new JRadioButton("螺旋形 (spiral)"); |
| | | |
| | | ButtonGroup group = new ButtonGroup(); |
| | | group.add(parallelBtn); |
| | | group.add(spiralBtn); |
| | | |
| | | if ("spiral".equals(normalized)) { |
| | | spiralBtn.setSelected(true); |
| | | } else { |
| | | parallelBtn.setSelected(true); |
| | | } |
| | | |
| | | JPanel panel = new JPanel(); |
| | | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); |
| | | panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); |
| | | panel.add(new JLabel("请选择割草模式:")); |
| | | panel.add(Box.createVerticalStrut(8)); |
| | | panel.add(parallelBtn); |
| | | panel.add(Box.createVerticalStrut(4)); |
| | | panel.add(spiralBtn); |
| | | |
| | | int option = JOptionPane.showConfirmDialog(this, panel, "编辑割草模式", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); |
| | | if (option != JOptionPane.OK_OPTION) { |
| | | return; |
| | | } |
| | | |
| | | String selectedValue = parallelBtn.isSelected() ? "parallel" : "spiral"; |
| | | if (!saveFieldAndRefresh(dikuai, "mowingPattern", selectedValue)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新割草模式", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | JOptionPane.showMessageDialog(this, "割草模式已更新", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private String normalizeExistingMowingPattern(String value) { |
| | | if (value == null) { |
| | | return "parallel"; |
| | | } |
| | | String trimmed = value.trim().toLowerCase(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return "parallel"; |
| | | } |
| | | switch (trimmed) { |
| | | case "1": |
| | | case "spiral": |
| | | case "螺旋": |
| | | case "螺旋模式": |
| | | return "spiral"; |
| | | case "0": |
| | | case "parallel": |
| | | case "平行": |
| | | case "平行模式": |
| | | default: |
| | | if (trimmed.contains("螺旋")) { |
| | | return "spiral"; |
| | | } |
| | | if (trimmed.contains("spiral")) { |
| | | return "spiral"; |
| | | } |
| | | if (trimmed.contains("parallel")) { |
| | | return "parallel"; |
| | | } |
| | | if (trimmed.contains("平行")) { |
| | | return "parallel"; |
| | | } |
| | | return "parallel"; |
| | | } |
| | | } |
| | | |
| | | private String sanitizeValueOrNull(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | /** |
| | | * 启动导航预览 |
| | | */ |
| | | private void startNavigationPreview(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // 获取地块管理对话框,准备在打开导航预览时关闭 |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // 关闭地块管理页面 |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | // 启动导航预览 |
| | | daohangyulan.getInstance().startNavigationPreview(dikuai); |
| | | } |
| | | |
| | | /** |
| | | * 显示路径规划页面 |
| | | */ |
| | | private void showPathPlanningPage(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // 获取地块管理对话框,准备在打开路径规划页面时关闭 |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // 获取地块基本数据 |
| | | String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates()); |
| | | String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates()); |
| | | List<Obstacledge.Obstacle> configuredObstacles = getConfiguredObstacles(dikuai); |
| | | String obstacleValue = determineInitialObstacleValue(dikuai, configuredObstacles); |
| | | String widthValue = sanitizeWidthString(dikuai.getMowingWidth()); |
| | | if (widthValue != null) { |
| | | try { |
| | | double widthCm = Double.parseDouble(widthValue); |
| | | widthValue = formatWidthForStorage(widthCm); |
| | | } catch (NumberFormatException ignored) { |
| | | // 保持原始字符串,稍后校验提示 |
| | | } |
| | | } |
| | | String modeValue = sanitizeValueOrNull(dikuai.getMowingPattern()); |
| | | String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath()); |
| | | |
| | | // 创建保存回调接口实现 |
| | | MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() { |
| | | @Override |
| | | public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) { |
| | | return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveMowingWidth(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "mowingWidth", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean savePlannedPath(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "plannedPath", value); |
| | | } |
| | | }; |
| | | |
| | | // 显示路径规划页面 |
| | | MowingPathGenerationPage dialog = new MowingPathGenerationPage( |
| | | owner, |
| | | dikuai, |
| | | baseStationValue, |
| | | boundaryValue, |
| | | obstacleValue, |
| | | widthValue, |
| | | modeValue, |
| | | existingPath, |
| | | callback |
| | | ); |
| | | |
| | | // 关闭地块管理页面 |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | |
| | | private void generateMowingPath(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates()); |
| | | String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates()); |
| | | List<Obstacledge.Obstacle> configuredObstacles = getConfiguredObstacles(dikuai); |
| | | String obstacleValue = determineInitialObstacleValue(dikuai, configuredObstacles); |
| | | String widthValue = sanitizeWidthString(dikuai.getMowingWidth()); |
| | | if (widthValue != null) { |
| | | try { |
| | | double widthCm = Double.parseDouble(widthValue); |
| | | widthValue = formatWidthForStorage(widthCm); |
| | | } catch (NumberFormatException ignored) { |
| | | // 保持原始字符串,稍后校验提示 |
| | | } |
| | | } |
| | | String modeValue = sanitizeValueOrNull(dikuai.getMowingPattern()); |
| | | String initialGenerated = attemptMowingPathPreview( |
| | | boundaryValue, |
| | | obstacleValue, |
| | | widthValue, |
| | | modeValue, |
| | | this, |
| | | false |
| | | ); |
| | | showMowingPathDialog(dikuai, baseStationValue, boundaryValue, obstacleValue, widthValue, modeValue, initialGenerated); |
| | | } |
| | | |
| | | private void showMowingPathDialog( |
| | | Dikuai dikuai, |
| | | String baseStationValue, |
| | | String boundaryValue, |
| | | String obstacleValue, |
| | | String widthValue, |
| | | String modeValue, |
| | | String initialGeneratedPath) { |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // 创建保存回调接口实现 |
| | | MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() { |
| | | @Override |
| | | public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) { |
| | | return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveMowingWidth(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "mowingWidth", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean savePlannedPath(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "plannedPath", value); |
| | | } |
| | | }; |
| | | |
| | | // 使用新的独立页面类 |
| | | MowingPathGenerationPage dialog = new MowingPathGenerationPage( |
| | | owner, |
| | | dikuai, |
| | | baseStationValue, |
| | | boundaryValue, |
| | | obstacleValue, |
| | | widthValue, |
| | | modeValue, |
| | | initialGeneratedPath, |
| | | callback |
| | | ); |
| | | |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | private String attemptMowingPathPreview( |
| | | String boundaryInput, |
| | | String obstacleInput, |
| | | String widthCmInput, |
| | | String modeInput, |
| | | Component parentComponent, |
| | | boolean showMessages) { |
| | | String boundary = sanitizeValueOrNull(boundaryInput); |
| | | if (boundary == null) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "当前地块未设置边界坐标,无法生成路径", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | String rawWidth = widthCmInput != null ? widthCmInput.trim() : ""; |
| | | String widthStr = sanitizeWidthString(widthCmInput); |
| | | if (widthStr == null) { |
| | | if (showMessages) { |
| | | String message = rawWidth.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确"; |
| | | JOptionPane.showMessageDialog(parentComponent, message, "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | double widthCm; |
| | | try { |
| | | widthCm = Double.parseDouble(widthStr); |
| | | } catch (NumberFormatException ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | if (widthCm <= 0) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | double widthMeters = widthCm / 100.0d; |
| | | String plannerWidth = BigDecimal.valueOf(widthMeters) |
| | | .setScale(3, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | String obstacles = sanitizeValueOrNull(obstacleInput); |
| | | if (obstacles != null) { |
| | | obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' '); |
| | | } |
| | | String mode = normalizeExistingMowingPattern(modeInput); |
| | | try { |
| | | String generated = Lunjingguihua.generatePathFromStrings(boundary, obstacles, plannerWidth, mode); |
| | | String trimmed = generated != null ? generated.trim() : ""; |
| | | if (trimmed.isEmpty()) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "未生成有效的割草路径,请检查地块数据", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "割草路径已生成", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | return trimmed; |
| | | } catch (IllegalArgumentException ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "生成割草路径失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } catch (Exception ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "生成割草路径时发生异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private JTextArea createInfoTextArea(String text, boolean editable, int rows) { |
| | | JTextArea area = new JTextArea(text); |
| | | area.setEditable(editable); |
| | | area.setLineWrap(true); |
| | | area.setWrapStyleWord(true); |
| | | area.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | area.setRows(Math.max(rows, 2)); |
| | | area.setCaretPosition(0); |
| | | area.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); |
| | | area.setBackground(editable ? WHITE : new Color(245, 245, 245)); |
| | | return area; |
| | | } |
| | | |
| | | private JPanel createTextAreaSection(String title, JTextArea textArea) { |
| | | JPanel section = new JPanel(new BorderLayout(0, 6)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | | scrollPane.getVerticalScrollBar().setUnitIncrement(12); |
| | | section.add(scrollPane, BorderLayout.CENTER); |
| | | |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); |
| | | return section; |
| | | } |
| | | |
| | | private JTextField createInfoTextField(String text, boolean editable) { |
| | | JTextField field = new JTextField(text); |
| | | field.setEditable(editable); |
| | | field.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | field.setForeground(TEXT_COLOR); |
| | | field.setBackground(editable ? WHITE : new Color(245, 245, 245)); |
| | | field.setCaretPosition(0); |
| | | field.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); |
| | | field.setFocusable(true); |
| | | field.setOpaque(true); |
| | | return field; |
| | | } |
| | | |
| | | private JPanel createTextFieldSection(String title, JTextField textField) { |
| | | JPanel section = new JPanel(new BorderLayout(0, 6)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | |
| | | JPanel fieldWrapper = new JPanel(new BorderLayout()); |
| | | fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245)); |
| | | fieldWrapper.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | | fieldWrapper.add(textField, BorderLayout.CENTER); |
| | | section.add(fieldWrapper, BorderLayout.CENTER); |
| | | |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); |
| | | return section; |
| | | } |
| | | |
| | | private JPanel createInfoValueSection(String title, String value) { |
| | | JPanel section = new JPanel(); |
| | | section.setLayout(new BoxLayout(section, BoxLayout.X_AXIS)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); |
| | | |
| | | JLabel titleLabel = new JLabel(title + ":"); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel); |
| | | section.add(Box.createHorizontalStrut(8)); |
| | | |
| | | JLabel valueLabel = new JLabel(value); |
| | | valueLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | valueLabel.setForeground(TEXT_COLOR); |
| | | section.add(valueLabel); |
| | | |
| | | section.add(Box.createHorizontalGlue()); |
| | | return section; |
| | | } |
| | | |
| | | private String formatMowingPatternForDisplay(String patternValue) { |
| | | String sanitized = sanitizeValueOrNull(patternValue); |
| | | if (sanitized == null) { |
| | | return "未设置"; |
| | | } |
| | | String normalized = normalizeExistingMowingPattern(sanitized); |
| | | if ("parallel".equals(normalized)) { |
| | | return "平行模式 (parallel)"; |
| | | } |
| | | if ("spiral".equals(normalized)) { |
| | | return "螺旋模式 (spiral)"; |
| | | } |
| | | return sanitized; |
| | | } |
| | | |
| | | private String formatMowingPatternForDialog(String patternValue) { |
| | | String sanitized = sanitizeValueOrNull(patternValue); |
| | | if (sanitized == null) { |
| | | return "未设置"; |
| | | } |
| | | String normalized = normalizeExistingMowingPattern(sanitized); |
| | | if ("parallel".equals(normalized)) { |
| | | return "平行模式 (parallel)"; |
| | | } |
| | | if ("spiral".equals(normalized)) { |
| | | return "螺旋模式 (spiral)"; |
| | | } |
| | | return sanitized; |
| | | } |
| | | |
| | | private List<Obstacledge.Obstacle> getConfiguredObstacles(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Obstacledge.Obstacle> obstacles = loadObstaclesFromConfig(dikuai.getLandNumber()); |
| | | if (obstacles == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return obstacles; |
| | | } |
| | | |
| | | private String determineInitialObstacleValue(Dikuai dikuai, List<Obstacledge.Obstacle> configuredObstacles) { |
| | | if (configuredObstacles != null && !configuredObstacles.isEmpty()) { |
| | | String payload = Obstacledge.buildPlannerPayload(configuredObstacles); |
| | | if (payload != null && !payload.trim().isEmpty()) { |
| | | return payload; |
| | | } |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | private String sanitizeWidthString(String input) { |
| | | if (input == null) { |
| | | return null; |
| | | } |
| | | String trimmed = input.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | String cleaned = trimmed.replaceAll("[^0-9.+-]", ""); |
| | | return cleaned.isEmpty() ? null : cleaned; |
| | | } |
| | | |
| | | private String formatWidthForStorage(double widthCm) { |
| | | return BigDecimal.valueOf(widthCm) |
| | | .setScale(2, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | } |
| | | |
| | | private boolean persistObstaclesForLand(Dikuai dikuai, String baseStationValue, String obstaclePayload) { |
| | | if (dikuai == null || dikuai.getLandNumber() == null) { |
| | | return false; |
| | | } |
| | | String landNumber = dikuai.getLandNumber().trim(); |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (configFile.exists()) { |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber); |
| | | if (plot == null) { |
| | | plot = new Obstacledge.Plot(landNumber); |
| | | manager.addPlot(plot); |
| | | } |
| | | |
| | | applyBaseStationValue(plot, baseStationValue, dikuai.getBaseStationCoordinates()); |
| | | |
| | | List<Obstacledge.Obstacle> obstacles; |
| | | if (obstaclePayload == null || "-1".equals(obstaclePayload.trim())) { |
| | | obstacles = new ArrayList<>(); |
| | | } else { |
| | | obstacles = Obstacledge.parsePlannerPayload(obstaclePayload, landNumber); |
| | | } |
| | | plot.setObstacles(obstacles); |
| | | |
| | | if (!manager.saveToFile(configFile.getAbsolutePath())) { |
| | | return false; |
| | | } |
| | | |
| | | obstacleSummaryCache = loadObstacleSummaries(); |
| | | boolean isCurrent = landNumber.equals(currentWorkLandNumber); |
| | | loadDikuaiData(); |
| | | if (isCurrent) { |
| | | setCurrentWorkLand(landNumber, dikuai.getLandName()); |
| | | } |
| | | return true; |
| | | } catch (Exception ex) { |
| | | System.err.println("保存障碍物配置失败: " + ex.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void applyBaseStationValue(Obstacledge.Plot plot, String baseStationValue, String fallbackValue) { |
| | | if (plot == null) { |
| | | return; |
| | | } |
| | | String sanitized = sanitizeBaseStationValue(baseStationValue); |
| | | if (sanitized == null) { |
| | | sanitized = sanitizeBaseStationValue(fallbackValue); |
| | | } |
| | | if (sanitized == null) { |
| | | return; |
| | | } |
| | | try { |
| | | plot.setBaseStationString(sanitized); |
| | | } catch (Exception ex) { |
| | | System.err.println("更新障碍物配置中的基站坐标失败: " + ex.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private String sanitizeBaseStationValue(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed.replaceAll("\\s+", ""); |
| | | } |
| | | |
| | | private String getDisplayValue(String value, String defaultValue) { |
| | | if (value == null || value.equals("-1") || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | |
| | | return value; |
| | | } |
| | | |
| | | private JButton createSmallButton(String text) { |
| | | JButton button = new JButton(text); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | button.setBackground(PRIMARY_COLOR); |
| | | button.setForeground(WHITE); |
| | | button.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10)); |
| | | button.setMargin(new Insets(0, 0, 0, 0)); |
| | | button.setFocusPainted(false); |
| | | button.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | |
| | | // 悬停效果 |
| | | button.addMouseListener(new MouseAdapter() { |
| | | public void mouseEntered(MouseEvent e) { |
| | | button.setBackground(PRIMARY_DARK); |
| | | } |
| | | public void mouseExited(MouseEvent e) { |
| | | button.setBackground(PRIMARY_COLOR); |
| | | } |
| | | /** |
| | | * 创建类似于链接的小按钮 |
| | | */ |
| | | private JButton createSmallLinkButton(String text, ActionListener listener) { |
| | | JButton btn = new JButton(text); |
| | | btn.setFont(new Font("微软雅黑", Font.PLAIN, 11)); |
| | | btn.setForeground(PRIMARY_COLOR); |
| | | btn.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(PRIMARY_COLOR, 1, true), |
| | | BorderFactory.createEmptyBorder(2, 6, 2, 6) |
| | | )); |
| | | btn.setContentAreaFilled(false); |
| | | btn.setFocusPainted(false); |
| | | btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | btn.addMouseListener(new MouseAdapter() { |
| | | public void mouseEntered(MouseEvent e) { btn.setOpaque(true); btn.setBackground(new Color(230, 250, 240)); } |
| | | public void mouseExited(MouseEvent e) { btn.setOpaque(false); } |
| | | }); |
| | | if (listener != null) { |
| | | btn.addActionListener(listener); |
| | | } |
| | | return btn; |
| | | } |
| | | |
| | | return button; |
| | | private JButton createSmallButton(String text) { |
| | | return createSmallLinkButton(text, null); |
| | | } |
| | | |
| | | private JButton createSmallButton(String text, Color backgroundColor, Color hoverColor) { |
| | | // 对于需要不同颜色的按钮,使用实心风格 |
| | | Color baseColor = backgroundColor == null ? PRIMARY_COLOR : backgroundColor; |
| | | return createStyledButton(text, baseColor, true); |
| | | } |
| | | |
| | | private JButton createActionButton(String text, Color color) { |
| | | JButton button = new JButton(text); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 14)); |
| | | button.setBackground(color); |
| | | button.setForeground(WHITE); |
| | | button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); |
| | | button.setFocusPainted(false); |
| | | button.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | return createStyledButton(text, color, true); // 实心风格 |
| | | } |
| | | |
| | | // 悬停效果 |
| | | button.addMouseListener(new MouseAdapter() { |
| | | public void mouseEntered(MouseEvent e) { |
| | | if (color == RED_COLOR) { |
| | | button.setBackground(RED_DARK); |
| | | /** |
| | | * 创建现代风格按钮 (实心/轮廓) |
| | | */ |
| | | private JButton createStyledButton(String text, Color baseColor, boolean filled) { |
| | | JButton btn = new JButton(text) { |
| | | @Override |
| | | protected void paintComponent(Graphics g) { |
| | | Graphics2D g2 = (Graphics2D) g.create(); |
| | | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
| | | |
| | | boolean isPressed = getModel().isPressed(); |
| | | boolean isRollover = getModel().isRollover(); |
| | | |
| | | if (filled) { |
| | | if (isPressed) g2.setColor(baseColor.darker()); |
| | | else if (isRollover) g2.setColor(baseColor.brighter()); |
| | | else g2.setColor(baseColor); |
| | | g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8); |
| | | g2.setColor(Color.WHITE); |
| | | } else { |
| | | button.setBackground(PRIMARY_DARK); |
| | | g2.setColor(CARD_BACKGROUND); // 背景 |
| | | g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8); |
| | | |
| | | if (isPressed) g2.setColor(baseColor.darker()); |
| | | else if (isRollover) g2.setColor(baseColor); |
| | | else g2.setColor(new Color(200, 200, 200)); // 默认边框灰 |
| | | |
| | | g2.setStroke(new BasicStroke(1.2f)); |
| | | g2.drawRoundRect(0, 0, getWidth()-1, getHeight()-1, 8, 8); |
| | | g2.setColor(isRollover ? baseColor : TEXT_COLOR); |
| | | } |
| | | |
| | | FontMetrics fm = g2.getFontMetrics(); |
| | | int x = (getWidth() - fm.stringWidth(getText())) / 2; |
| | | int y = (getHeight() - fm.getHeight()) / 2 + fm.getAscent(); |
| | | g2.drawString(getText(), x, y); |
| | | |
| | | g2.dispose(); |
| | | } |
| | | public void mouseExited(MouseEvent e) { |
| | | if (color == RED_COLOR) { |
| | | button.setBackground(RED_COLOR); |
| | | } else { |
| | | button.setBackground(PRIMARY_COLOR); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return button; |
| | | }; |
| | | btn.setFocusPainted(false); |
| | | btn.setContentAreaFilled(false); |
| | | btn.setBorderPainted(false); |
| | | btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | btn.setFont(new Font("微软雅黑", Font.BOLD, 12)); |
| | | return btn; |
| | | } |
| | | |
| | | private JButton createDeleteButton() { |
| | | JButton button = new JButton("删除"); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | button.setBackground(RED_COLOR); |
| | | button.setForeground(WHITE); |
| | | button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12)); |
| | | button.setFocusPainted(false); |
| | | button.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | |
| | | ImageIcon deleteIcon = loadIcon("image/delete.png", 16, 16); |
| | | JButton button = new JButton(); |
| | | ImageIcon deleteIcon = loadIcon("image/delete.png", 25, 25); |
| | | if (deleteIcon != null) { |
| | | button.setIcon(deleteIcon); |
| | | button.setIconTextGap(6); |
| | | } else { |
| | | button.setText("删除"); |
| | | } |
| | | |
| | | // 悬停效果 |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 11)); |
| | | button.setForeground(RED_COLOR); |
| | | button.setBorder(BorderFactory.createEmptyBorder()); |
| | | button.setContentAreaFilled(false); |
| | | button.setFocusPainted(false); |
| | | button.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | button.addMouseListener(new MouseAdapter() { |
| | | public void mouseEntered(MouseEvent e) { |
| | | button.setBackground(RED_DARK); |
| | | } |
| | | public void mouseExited(MouseEvent e) { |
| | | button.setBackground(RED_COLOR); |
| | | } |
| | | public void mouseEntered(MouseEvent e) { button.setOpaque(true); button.setBackground(new Color(255, 240, 240)); } |
| | | public void mouseExited(MouseEvent e) { button.setOpaque(false); } |
| | | }); |
| | | |
| | | return button; |
| | | } |
| | | |
| | | private JButton createViewButton(ActionListener listener) { |
| | | // 使用 Lookbutton 类创建查看按钮 |
| | | return Lookbutton.createViewButton(listener, new Color(230, 250, 240)); |
| | | } |
| | | |
| | | private JButton createPrimaryFooterButton(String text) { |
| | | return createStyledButton(text, PRIMARY_COLOR, true); // 实心风格 |
| | | } |
| | | |
| | | private JButton createWorkToggleButton(Dikuai dikuai) { |
| | | JButton button = new JButton(); |
| | | button.setContentAreaFilled(false); |
| | |
| | | button.setText(isCurrent ? "当前地块" : "设为当前"); |
| | | } |
| | | button.setToolTipText(isCurrent ? "取消当前作业地块" : "设为当前作业地块"); |
| | | |
| | | // 更新状态文字标签的显示/隐藏 |
| | | Object statusLabelObj = button.getClientProperty("statusLabel"); |
| | | if (statusLabelObj instanceof JLabel) { |
| | | JLabel statusLabel = (JLabel) statusLabelObj; |
| | | statusLabel.setVisible(isCurrent); |
| | | } |
| | | } |
| | | |
| | | private void ensureWorkIconsLoaded() { |
| | |
| | | } |
| | | |
| | | public static void setCurrentWorkLand(String landNumber, String landName) { |
| | | currentWorkLandNumber = landNumber; |
| | | String sanitizedLandNumber = sanitizeLandNumber(landNumber); |
| | | boolean changed = !Objects.equals(currentWorkLandNumber, sanitizedLandNumber); |
| | | if (!changed) { |
| | | String persisted = readPersistedWorkLandNumber(); |
| | | if (!Objects.equals(persisted, sanitizedLandNumber)) { |
| | | changed = true; |
| | | } |
| | | } |
| | | currentWorkLandNumber = sanitizedLandNumber; |
| | | |
| | | Dikuai dikuai = null; |
| | | if (landNumber != null) { |
| | | dikuai = Dikuai.getDikuai(landNumber); |
| | | if (sanitizedLandNumber != null) { |
| | | dikuai = Dikuai.getDikuai(sanitizedLandNumber); |
| | | if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) { |
| | | landName = dikuai.getLandName(); |
| | | } |
| | |
| | | |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | if (landNumber == null) { |
| | | if (sanitizedLandNumber == null) { |
| | | shouye.updateCurrentAreaName(null); |
| | | } else { |
| | | shouye.updateCurrentAreaName(landName); |
| | | } |
| | | MapRenderer renderer = shouye.getMapRenderer(); |
| | | if (renderer != null) { |
| | | renderer.applyLandMetadata(dikuai); |
| | | String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null; |
| | | String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null; |
| | | String obstacles = (dikuai != null) ? dikuai.getObstacleCoordinates() : null; |
| | | renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName); |
| | | List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList(); |
| | | if (configuredObstacles == null) { |
| | | configuredObstacles = Collections.emptyList(); |
| | | } |
| | | renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName); |
| | | renderer.setCurrentPlannedPath(plannedPath); |
| | | renderer.setCurrentObstacles(obstacles, landNumber); |
| | | boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false); |
| | | renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber); |
| | | boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false); |
| | | renderer.setBoundaryPointsVisible(showBoundaryPoints); |
| | | renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d); |
| | | // 退出预览后,不显示障碍物点(障碍物点只在预览时显示) |
| | | renderer.setObstaclePointsVisible(false); |
| | | } |
| | | shouye.refreshMowingIndicators(); |
| | | } |
| | | |
| | | if (changed) { |
| | | persistCurrentWorkLand(sanitizedLandNumber); |
| | | } |
| | | } |
| | | |
| | |
| | | return currentWorkLandNumber; |
| | | } |
| | | |
| | | public static String getPersistedWorkLandNumber() { |
| | | return readPersistedWorkLandNumber(); |
| | | } |
| | | |
| | | private static String sanitizeLandNumber(String landNumber) { |
| | | if (landNumber == null) { |
| | | return null; |
| | | } |
| | | String trimmed = landNumber.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private static void persistCurrentWorkLand(String landNumber) { |
| | | synchronized (Dikuaiguanli.class) { |
| | | Properties props = new Properties(); |
| | | try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) { |
| | | props.load(in); |
| | | } catch (IOException ignored) { |
| | | // Use empty defaults when the configuration file is missing. |
| | | } |
| | | |
| | | if (landNumber == null) { |
| | | props.setProperty(WORK_LAND_KEY, "-1"); |
| | | } else { |
| | | props.setProperty(WORK_LAND_KEY, landNumber); |
| | | } |
| | | |
| | | try (FileOutputStream out = new FileOutputStream(PROPERTIES_FILE)) { |
| | | props.store(out, "Current work land selection updated"); |
| | | } catch (IOException ex) { |
| | | System.err.println("无法保存当前作业地块: " + ex.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static String readPersistedWorkLandNumber() { |
| | | Properties props = new Properties(); |
| | | try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) { |
| | | props.load(in); |
| | | String value = props.getProperty(WORK_LAND_KEY); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } catch (IOException ex) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private ImageIcon loadIcon(String path, int width, int height) { |
| | | try { |
| | | ImageIcon rawIcon = new ImageIcon(path); |
| | |
| | | }); |
| | | } |
| | | |
| | | private void editReturnPoint(Dikuai dikuai) { |
| | | FanhuiDialog fd = new FanhuiDialog(SwingUtilities.getWindowAncestor(this), dikuai); |
| | | fd.setVisible(true); |
| | | // 如果对话框已更新数据,刷新显示 |
| | | if (fd.isUpdated()) { |
| | | loadDikuaiData(); |
| | | private void editReturnPath(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String edited = promptCoordinateEditing("查看 / 编辑往返点路径", dikuai.getReturnPathCoordinates(), dikuai); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | // 检查是否是特殊标记,表示点击了"去绘制"按钮 |
| | | if ("__OPEN_DRAW_PAGE__".equals(edited) || "__OPEN_DRAW_PAGE_REFRESH__".equals(edited)) { |
| | | // 获取地块管理对话框 |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // 获取地块管理对话框的父窗口(主窗口),作为绘制页面的父窗口 |
| | | Window drawPageOwner = null; |
| | | if (managementWindow != null) { |
| | | drawPageOwner = managementWindow.getOwner(); |
| | | } |
| | | if (drawPageOwner == null && owner != null) { |
| | | drawPageOwner = owner.getOwner(); |
| | | } |
| | | if (drawPageOwner == null) { |
| | | drawPageOwner = owner; |
| | | } |
| | | |
| | | // 先关闭地块管理页面 |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | // 然后打开绘制页面,如果是重新绘制,传递标记 |
| | | boolean isRefresh = "__OPEN_DRAW_PAGE_REFRESH__".equals(edited); |
| | | Huizhiwanfanpath.showDrawReturnPathDialog(drawPageOwner, dikuai, isRefresh); |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "returnPathCoordinates", normalized)) { |
| | | JOptionPane.showMessageDialog(this, "无法更新往返点路径", "错误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "往返点路径已清空" : "往返点路径已更新"; |
| | | JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | /** |
| | | * 显示障碍物管理页面 |
| | | */ |
| | | private void showObstacleManagementPage(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // 获取地块管理对话框,准备在打开障碍物管理页面时关闭 |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | ObstacleManagementPage managementPage = new ObstacleManagementPage(owner, dikuai); |
| | | |
| | | // 关闭地块管理页面 |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | managementPage.setVisible(true); |
| | | } |
| | | |
| | | private void addNewObstacle(Dikuai dikuai) { |
| | |
| | | loadDikuaiData(); |
| | | } |
| | | |
| | | private void showCompletedMowingTrackDialog(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | JDialog dialog = new JDialog(owner, "已完成的割草路径坐标", Dialog.ModalityType.APPLICATION_MODAL); |
| | | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | dialog.setLayout(new BorderLayout()); |
| | | dialog.getContentPane().setBackground(WHITE); |
| | | |
| | | String normalizedTrack = prepareCoordinateForEditor(dikuai.getMowingTrack()); |
| | | boolean hasTrack = normalizedTrack != null && !normalizedTrack.isEmpty(); |
| | | |
| | | JTextArea textArea = new JTextArea(hasTrack ? normalizedTrack : ""); |
| | | textArea.setEditable(false); |
| | | textArea.setLineWrap(true); |
| | | textArea.setWrapStyleWord(true); |
| | | textArea.setFont(new Font("微软雅黑", Font.PLAIN, 13)); |
| | | textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); |
| | | textArea.setCaretPosition(0); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setPreferredSize(new Dimension(342, 240)); |
| | | scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12)); |
| | | |
| | | JLabel statusLabel = new JLabel(hasTrack ? "当前已保存完成的割草路径记录。" : "当前暂无完成的割草路径记录。"); |
| | | statusLabel.setBorder(BorderFactory.createEmptyBorder(12, 12, 6, 12)); |
| | | statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | statusLabel.setForeground(hasTrack ? TEXT_COLOR : LIGHT_TEXT); |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); |
| | | buttonPanel.setBackground(WHITE); |
| | | buttonPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); |
| | | |
| | | JButton deleteButton = createSmallButton("删除记录", RED_COLOR, RED_DARK); |
| | | deleteButton.setEnabled(hasTrack); |
| | | JButton closeButton = createSmallButton("关闭", PRIMARY_COLOR, PRIMARY_DARK); |
| | | |
| | | deleteButton.addActionListener(e -> { |
| | | String latestTrack = prepareCoordinateForEditor(dikuai.getMowingTrack()); |
| | | if (latestTrack == null || latestTrack.isEmpty()) { |
| | | JOptionPane.showMessageDialog(dialog, "当前没有可删除的轨迹记录。", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | int choice = JOptionPane.showConfirmDialog(dialog, "确定要删除已完成的割草路径记录吗?", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); |
| | | if (choice != JOptionPane.YES_OPTION) { |
| | | return; |
| | | } |
| | | if (saveFieldAndRefresh(dikuai, "mowingTrack", "-1")) { |
| | | dikuai.setMowingTrack("-1"); |
| | | textArea.setText(""); |
| | | statusLabel.setText("当前暂无完成的割草路径记录。"); |
| | | statusLabel.setForeground(LIGHT_TEXT); |
| | | deleteButton.setEnabled(false); |
| | | JOptionPane.showMessageDialog(dialog, "已删除完成路径记录", "成功", JOptionPane.INFORMATION_MESSAGE); |
| | | } else { |
| | | JOptionPane.showMessageDialog(dialog, "删除失败,请稍后重试。", "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | }); |
| | | |
| | | closeButton.addActionListener(e -> dialog.dispose()); |
| | | |
| | | buttonPanel.add(deleteButton); |
| | | buttonPanel.add(closeButton); |
| | | |
| | | dialog.add(statusLabel, BorderLayout.NORTH); |
| | | dialog.add(scrollPane, BorderLayout.CENTER); |
| | | dialog.add(buttonPanel, BorderLayout.SOUTH); |
| | | dialog.pack(); |
| | | dialog.setMinimumSize(new Dimension(378, 320)); |
| | | dialog.setLocationRelativeTo(owner); |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | private Map<String, ObstacleSummary> loadObstacleSummaries() { |
| | | Map<String, ObstacleSummary> summaries = new HashMap<>(); |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return summaries; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return summaries; |
| | | } |
| | | for (Obstacledge.Plot plot : manager.getPlots()) { |
| | | if (plot == null) { |
| | | continue; |
| | | } |
| | | String plotId = plot.getPlotId(); |
| | | if (plotId == null || plotId.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | List<String> names = new ArrayList<>(); |
| | | for (Obstacledge.Obstacle obstacle : plot.getObstacles()) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String name = obstacle.getObstacleName(); |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (!trimmed.isEmpty()) { |
| | | names.add(trimmed); |
| | | } |
| | | } |
| | | summaries.put(plotId.trim(), ObstacleSummary.of(names)); |
| | | } |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | } |
| | | return summaries; |
| | | } |
| | | |
| | | private static List<Obstacledge.Obstacle> loadObstaclesFromConfig(String landNumber) { |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return null; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return null; |
| | | } |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); |
| | | if (plot == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Obstacledge.Obstacle> obstacles = plot.getObstacles(); |
| | | if (obstacles == null || obstacles.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return new ArrayList<>(obstacles); |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private ObstacleSummary getObstacleSummaryFromCache(String landNumber) { |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return ObstacleSummary.empty(); |
| | | } |
| | | if (obstacleSummaryCache == null || obstacleSummaryCache.isEmpty()) { |
| | | return ObstacleSummary.empty(); |
| | | } |
| | | ObstacleSummary summary = obstacleSummaryCache.get(landNumber.trim()); |
| | | return summary != null ? summary : ObstacleSummary.empty(); |
| | | } |
| | | |
| | | private List<String> loadObstacleNamesForLand(String landNumber) { |
| | | List<String> names = new ArrayList<>(); |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return names; |
| | | } |
| | | try { |
| | | File configFile = new File("Obstacledge.properties"); |
| | | if (!configFile.exists()) { |
| | | return names; |
| | | } |
| | | Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); |
| | | if (!manager.loadFromFile(configFile.getAbsolutePath())) { |
| | | return names; |
| | | } |
| | | Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); |
| | | if (plot == null) { |
| | | return names; |
| | | } |
| | | for (Obstacledge.Obstacle obstacle : plot.getObstacles()) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String name = obstacle.getObstacleName(); |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (!trimmed.isEmpty() && !names.contains(trimmed)) { |
| | | names.add(trimmed); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | System.err.println("读取障碍物配置失败: " + ex.getMessage()); |
| | | ObstacleSummary cached = getObstacleSummaryFromCache(landNumber); |
| | | if (!cached.isEmpty()) { |
| | | names.addAll(cached.copyNames()); |
| | | return names; |
| | | } |
| | | Map<String, ObstacleSummary> latest = loadObstacleSummaries(); |
| | | if (!latest.isEmpty()) { |
| | | obstacleSummaryCache = latest; |
| | | } |
| | | ObstacleSummary refreshed = getObstacleSummaryFromCache(landNumber); |
| | | if (!refreshed.isEmpty()) { |
| | | names.addAll(refreshed.copyNames()); |
| | | } |
| | | return names; |
| | | } |
| | |
| | | } |
| | | boundaryPointVisibility.put(landNumber, visible); |
| | | } |
| | | |
| | | private static final class ObstacleSummary { |
| | | private static final ObstacleSummary EMPTY = new ObstacleSummary(Collections.emptyList()); |
| | | private final List<String> names; |
| | | |
| | | private ObstacleSummary(List<String> names) { |
| | | this.names = names; |
| | | } |
| | | |
| | | static ObstacleSummary of(List<String> originalNames) { |
| | | if (originalNames == null || originalNames.isEmpty()) { |
| | | return empty(); |
| | | } |
| | | List<String> cleaned = new ArrayList<>(); |
| | | for (String name : originalNames) { |
| | | if (name == null) { |
| | | continue; |
| | | } |
| | | String trimmed = name.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | continue; |
| | | } |
| | | boolean duplicated = false; |
| | | for (String existing : cleaned) { |
| | | if (existing.equalsIgnoreCase(trimmed)) { |
| | | duplicated = true; |
| | | break; |
| | | } |
| | | } |
| | | if (!duplicated) { |
| | | cleaned.add(trimmed); |
| | | } |
| | | } |
| | | if (cleaned.isEmpty()) { |
| | | return empty(); |
| | | } |
| | | cleaned.sort(String::compareToIgnoreCase); |
| | | return new ObstacleSummary(Collections.unmodifiableList(cleaned)); |
| | | } |
| | | |
| | | static ObstacleSummary empty() { |
| | | return EMPTY; |
| | | } |
| | | |
| | | boolean isEmpty() { |
| | | return names.isEmpty(); |
| | | } |
| | | |
| | | int count() { |
| | | return names.size(); |
| | | } |
| | | |
| | | String buildDisplayValue() { |
| | | return count() > 0 ? String.format("障碍物%d个", count()) : "暂无障碍物"; |
| | | } |
| | | |
| | | String buildTooltip() { |
| | | return count() > 0 ? String.join(",", names) : "暂无障碍物"; |
| | | } |
| | | |
| | | List<String> copyNames() { |
| | | return new ArrayList<>(names); |
| | | } |
| | | } |
| | | |
| | | |
| | | } |