package zhuye;
|
|
import javax.swing.*;
|
import java.awt.*;
|
import java.awt.event.*;
|
import java.awt.geom.AffineTransform;
|
import java.awt.geom.Point2D;
|
import java.awt.geom.Rectangle2D;
|
import java.math.BigDecimal;
|
import java.text.SimpleDateFormat;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.List;
|
import java.util.Locale;
|
import gecaoji.Device;
|
import gecaoji.Gecaoji;
|
import dikuai.Dikuaiguanli;
|
import dikuai.Dikuai;
|
import zhangaiwu.Obstacledraw;
|
import zhangaiwu.Obstacledge;
|
|
/**
|
* 地图渲染器 - 负责坐标系绘制、视图变换等功能
|
*/
|
public class MapRenderer {
|
// 视图变换参数
|
private double scale = 1.0;
|
private double translateX = 0.0;
|
private double translateY = 0.0;
|
private Point lastDragPoint;
|
private Point mousePoint;
|
|
// 主题颜色
|
private final Color THEME_COLOR = new Color(46, 139, 87);
|
private final Color BACKGROUND_COLOR = new Color(250, 250, 250);
|
private static final Color GRASS_FILL_COLOR = new Color(144, 238, 144, 120);
|
private static final Color GRASS_BORDER_COLOR = new Color(60, 179, 113);
|
private static final Color BOUNDARY_POINT_COLOR = new Color(128, 0, 128);
|
private static final Color BOUNDARY_LABEL_COLOR = Color.BLACK;
|
private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05;
|
private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
// 组件引用
|
private JPanel visualizationPanel;
|
private List<Point2D.Double> currentBoundary;
|
private Rectangle2D.Double boundaryBounds;
|
private List<Point2D.Double> currentPlannedPath;
|
private Rectangle2D.Double plannedPathBounds;
|
private List<Obstacledge.Obstacle> currentObstacles;
|
private Rectangle2D.Double obstacleBounds;
|
private String selectedObstacleName;
|
private String boundaryName;
|
private boolean boundaryPointsVisible;
|
private String currentBoundaryLandNumber;
|
private boolean dragInProgress;
|
private final Gecaoji mower;
|
private final Timer mowerUpdateTimer;
|
private JDialog mowerInfoDialog;
|
private Timer mowerInfoRefreshTimer;
|
private JLabel mowerNumberValueLabel;
|
private JLabel realtimeXValueLabel;
|
private JLabel realtimeYValueLabel;
|
private JLabel positioningStatusValueLabel;
|
private JLabel satelliteCountValueLabel;
|
private JLabel realtimeSpeedValueLabel;
|
private JLabel headingValueLabel;
|
private JLabel updateTimeValueLabel;
|
|
public MapRenderer(JPanel visualizationPanel) {
|
this.visualizationPanel = visualizationPanel;
|
this.mower = new Gecaoji();
|
this.mowerUpdateTimer = createMowerTimer();
|
setupMouseListeners();
|
}
|
|
/**
|
* 设置鼠标监听器
|
*/
|
private void setupMouseListeners() {
|
// 鼠标滚轮缩放
|
visualizationPanel.addMouseWheelListener(e -> {
|
mousePoint = e.getPoint();
|
int notches = e.getWheelRotation();
|
double zoomFactor = notches < 0 ? 1.2 : 1/1.2;
|
zoomAtPoint(zoomFactor);
|
});
|
|
// 鼠标拖拽移动
|
visualizationPanel.addMouseListener(new MouseAdapter() {
|
public void mousePressed(MouseEvent e) {
|
if (SwingUtilities.isRightMouseButton(e)) {
|
resetView();
|
} else {
|
dragInProgress = false;
|
lastDragPoint = e.getPoint();
|
}
|
}
|
|
public void mouseReleased(MouseEvent e) {
|
lastDragPoint = null;
|
dragInProgress = false;
|
}
|
|
public void mouseClicked(MouseEvent e) {
|
if (dragInProgress) {
|
dragInProgress = false;
|
return;
|
}
|
if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() != 1) {
|
return;
|
}
|
if (handleMowerClick(e.getPoint())) {
|
return;
|
}
|
if (boundaryPointsVisible) {
|
handleBoundaryPointClick(e.getPoint());
|
}
|
}
|
});
|
|
visualizationPanel.addMouseMotionListener(new MouseAdapter() {
|
public void mouseDragged(MouseEvent e) {
|
if (lastDragPoint != null && !SwingUtilities.isRightMouseButton(e)) {
|
int dx = e.getX() - lastDragPoint.x;
|
int dy = e.getY() - lastDragPoint.y;
|
|
translateX += dx / scale;
|
translateY += dy / scale;
|
|
lastDragPoint = e.getPoint();
|
dragInProgress = true;
|
visualizationPanel.repaint();
|
}
|
}
|
|
public void mouseMoved(MouseEvent e) {
|
// 更新鼠标位置用于显示坐标
|
mousePoint = e.getPoint();
|
visualizationPanel.repaint();
|
}
|
});
|
}
|
|
private Timer createMowerTimer() {
|
Timer timer = new Timer(300, e -> {
|
mower.refreshFromDevice();
|
if (visualizationPanel != null) {
|
visualizationPanel.repaint();
|
}
|
});
|
timer.setInitialDelay(0);
|
timer.setRepeats(true);
|
timer.start();
|
return timer;
|
}
|
|
/**
|
* 基于鼠标位置的缩放
|
*/
|
private void zoomAtPoint(double zoomFactor) {
|
if (mousePoint == null) return;
|
|
// 计算鼠标位置在世界坐标系中的坐标
|
double worldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
|
double worldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
|
|
scale *= zoomFactor;
|
scale = Math.max(0.05, Math.min(scale, 50.0)); // 限制缩放范围,允许最多缩小至原始的1/20
|
|
// 计算缩放后同一鼠标位置在世界坐标系中的新坐标
|
double newWorldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
|
double newWorldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
|
|
// 调整平移量,使鼠标指向的世界坐标保持不变
|
translateX += (newWorldX - worldX);
|
translateY += (newWorldY - worldY);
|
|
visualizationPanel.repaint();
|
}
|
|
/**
|
* 重置视图
|
*/
|
public void resetView() {
|
scale = 1.0;
|
translateX = 0.0;
|
translateY = 0.0;
|
visualizationPanel.repaint();
|
}
|
|
/**
|
* 绘制地图内容
|
*/
|
public void renderMap(Graphics g) {
|
Graphics2D g2d = (Graphics2D) g;
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
// 保存原始变换
|
AffineTransform originalTransform = g2d.getTransform();
|
|
// 应用视图变换
|
g2d.translate(visualizationPanel.getWidth()/2, visualizationPanel.getHeight()/2);
|
g2d.scale(scale, scale);
|
g2d.translate(translateX, translateY);
|
|
// 绘制坐标系内容
|
drawCoordinateSystem(g2d);
|
|
boolean hasBoundary = currentBoundary != null && currentBoundary.size() >= 2;
|
boolean hasPlannedPath = currentPlannedPath != null && currentPlannedPath.size() >= 2;
|
boolean hasObstacles = currentObstacles != null && !currentObstacles.isEmpty();
|
|
if (hasBoundary) {
|
drawCurrentBoundary(g2d);
|
}
|
|
if (hasObstacles) {
|
Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName);
|
}
|
|
if (hasPlannedPath) {
|
drawCurrentPlannedPath(g2d);
|
}
|
|
if (boundaryPointsVisible && hasBoundary) {
|
pointandnumber.drawBoundaryPoints(
|
g2d,
|
currentBoundary,
|
scale,
|
BOUNDARY_POINT_MERGE_THRESHOLD,
|
BOUNDARY_POINT_COLOR,
|
BOUNDARY_LABEL_COLOR
|
);
|
}
|
|
drawMower(g2d);
|
|
// 恢复原始变换
|
g2d.setTransform(originalTransform);
|
|
// 绘制视图信息
|
drawViewInfo(g2d);
|
}
|
|
/**
|
* 绘制坐标系
|
*/
|
private void drawCoordinateSystem(Graphics2D g2d) {
|
// 绘制原点 - 红色实心小圆圈
|
g2d.setColor(Color.RED);
|
g2d.fillOval(-1, -1, 2, 2);
|
}
|
|
|
|
/**
|
* 绘制割草机
|
*/
|
private void drawMower(Graphics2D g2d) {
|
mower.draw(g2d, scale);
|
}
|
|
private boolean handleMowerClick(Point screenPoint) {
|
if (!mower.hasValidPosition()) {
|
return false;
|
}
|
Point2D.Double mowerPosition = mower.getPosition();
|
if (mowerPosition == null) {
|
return false;
|
}
|
Point2D.Double worldPoint = screenToWorld(screenPoint);
|
double radius = mower.getWorldRadius(scale);
|
if (Double.isNaN(radius)) {
|
return false;
|
}
|
double dx = worldPoint.x - mowerPosition.x;
|
double dy = worldPoint.y - mowerPosition.y;
|
if (dx * dx + dy * dy <= radius * radius) {
|
showMowerInfo();
|
return true;
|
}
|
return false;
|
}
|
|
private Point2D.Double screenToWorld(Point screenPoint) {
|
double worldX = (screenPoint.x - visualizationPanel.getWidth() / 2.0) / scale - translateX;
|
double worldY = (screenPoint.y - visualizationPanel.getHeight() / 2.0) / scale - translateY;
|
return new Point2D.Double(worldX, worldY);
|
}
|
|
private void drawCurrentBoundary(Graphics2D g2d) {
|
bianjiedrwa.drawBoundary(g2d, currentBoundary, scale, GRASS_FILL_COLOR, GRASS_BORDER_COLOR);
|
}
|
|
private void drawCurrentPlannedPath(Graphics2D g2d) {
|
lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale);
|
}
|
|
private void showMowerInfo() {
|
Device device = Device.getGecaoji();
|
if (device == null) {
|
JOptionPane.showMessageDialog(
|
visualizationPanel,
|
"无设备数据",
|
"割草机信息",
|
JOptionPane.INFORMATION_MESSAGE
|
);
|
return;
|
}
|
|
ensureMowerInfoDialog();
|
updateMowerInfoLabels();
|
positionMowerDialog();
|
if (!mowerInfoDialog.isVisible()) {
|
mowerInfoDialog.setVisible(true);
|
} else {
|
mowerInfoDialog.toFront();
|
}
|
startMowerInfoTimer();
|
}
|
|
private void ensureMowerInfoDialog() {
|
if (mowerInfoDialog != null) {
|
return;
|
}
|
|
Window owner = SwingUtilities.getWindowAncestor(visualizationPanel);
|
JDialog dialog = new JDialog(owner, "割草机信息", Dialog.ModalityType.MODELESS);
|
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
|
JPanel content = new JPanel(new BorderLayout(0, 12));
|
content.setBorder(BorderFactory.createEmptyBorder(16, 20, 16, 20));
|
|
JPanel grid = new JPanel(new GridLayout(0, 2, 12, 8));
|
|
grid.add(new JLabel("设备编号:"));
|
mowerNumberValueLabel = createValueLabel();
|
grid.add(mowerNumberValueLabel);
|
|
grid.add(new JLabel("实时X坐标:"));
|
realtimeXValueLabel = createValueLabel();
|
grid.add(realtimeXValueLabel);
|
|
grid.add(new JLabel("实时Y坐标:"));
|
realtimeYValueLabel = createValueLabel();
|
grid.add(realtimeYValueLabel);
|
|
grid.add(new JLabel("定位质量:"));
|
positioningStatusValueLabel = createValueLabel();
|
grid.add(positioningStatusValueLabel);
|
|
grid.add(new JLabel("卫星颗数:"));
|
satelliteCountValueLabel = createValueLabel();
|
grid.add(satelliteCountValueLabel);
|
|
grid.add(new JLabel("行驶速度:"));
|
realtimeSpeedValueLabel = createValueLabel();
|
grid.add(realtimeSpeedValueLabel);
|
|
grid.add(new JLabel("运动航向:"));
|
headingValueLabel = createValueLabel();
|
grid.add(headingValueLabel);
|
|
grid.add(new JLabel("更新时间:"));
|
updateTimeValueLabel = createValueLabel();
|
grid.add(updateTimeValueLabel);
|
|
content.add(grid, BorderLayout.CENTER);
|
|
JButton closeButton = new JButton("关闭");
|
closeButton.addActionListener(e -> dialog.dispose());
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
buttonPanel.add(closeButton);
|
content.add(buttonPanel, BorderLayout.SOUTH);
|
|
dialog.setContentPane(content);
|
dialog.pack();
|
dialog.setResizable(false);
|
dialog.addWindowListener(new WindowAdapter() {
|
@Override
|
public void windowClosed(WindowEvent e) {
|
stopMowerInfoTimer();
|
resetMowerInfoLabels();
|
mowerInfoDialog = null;
|
}
|
});
|
|
mowerInfoDialog = dialog;
|
}
|
|
private void positionMowerDialog() {
|
if (mowerInfoDialog == null || visualizationPanel == null) {
|
return;
|
}
|
int panelWidth = visualizationPanel.getWidth();
|
int panelHeight = visualizationPanel.getHeight();
|
int dialogHeight = mowerInfoDialog.getHeight();
|
if (dialogHeight <= 0) {
|
dialogHeight = mowerInfoDialog.getPreferredSize().height;
|
}
|
if (panelWidth > 0) {
|
mowerInfoDialog.setSize(panelWidth, dialogHeight);
|
}
|
|
try {
|
Point panelOnScreen = visualizationPanel.getLocationOnScreen();
|
int dialogWidth = mowerInfoDialog.getWidth();
|
int targetX = panelOnScreen.x + (panelWidth - dialogWidth) / 2;
|
int targetY = panelOnScreen.y + panelHeight / 3 - dialogHeight / 2;
|
mowerInfoDialog.setLocation(targetX, targetY);
|
} catch (IllegalComponentStateException ex) {
|
// component not yet showing; ignore positioning
|
}
|
}
|
|
private JLabel createValueLabel() {
|
JLabel label = new JLabel("--");
|
label.setHorizontalAlignment(SwingConstants.LEFT);
|
return label;
|
}
|
|
private void startMowerInfoTimer() {
|
if (mowerInfoRefreshTimer == null) {
|
mowerInfoRefreshTimer = new Timer(1000, e -> updateMowerInfoLabels());
|
mowerInfoRefreshTimer.setRepeats(true);
|
}
|
if (!mowerInfoRefreshTimer.isRunning()) {
|
mowerInfoRefreshTimer.start();
|
}
|
}
|
|
private void stopMowerInfoTimer() {
|
if (mowerInfoRefreshTimer != null) {
|
mowerInfoRefreshTimer.stop();
|
mowerInfoRefreshTimer = null;
|
}
|
}
|
|
private void resetMowerInfoLabels() {
|
mowerNumberValueLabel = null;
|
realtimeXValueLabel = null;
|
realtimeYValueLabel = null;
|
positioningStatusValueLabel = null;
|
satelliteCountValueLabel = null;
|
realtimeSpeedValueLabel = null;
|
headingValueLabel = null;
|
updateTimeValueLabel = null;
|
}
|
|
private void updateMowerInfoLabels() {
|
if (mowerNumberValueLabel == null) {
|
return;
|
}
|
|
Device device = Device.getGecaoji();
|
if (device == null) {
|
setMowerInfoLabels("--");
|
return;
|
}
|
|
mower.refreshFromDevice();
|
|
mowerNumberValueLabel.setText(formatDeviceValue(device.getMowerNumber()));
|
realtimeXValueLabel.setText(formatDeviceValue(device.getRealtimeX()));
|
realtimeYValueLabel.setText(formatDeviceValue(device.getRealtimeY()));
|
positioningStatusValueLabel.setText(formatDeviceValue(device.getPositioningStatus()));
|
satelliteCountValueLabel.setText(formatDeviceValue(device.getSatelliteCount()));
|
realtimeSpeedValueLabel.setText(formatDeviceValue(device.getRealtimeSpeed()));
|
headingValueLabel.setText(formatDeviceValue(device.getHeading()));
|
updateTimeValueLabel.setText(formatTimestamp(device.getGupdateTime()));
|
}
|
|
private void setMowerInfoLabels(String value) {
|
if (mowerNumberValueLabel != null) mowerNumberValueLabel.setText(value);
|
if (realtimeXValueLabel != null) realtimeXValueLabel.setText(value);
|
if (realtimeYValueLabel != null) realtimeYValueLabel.setText(value);
|
if (positioningStatusValueLabel != null) positioningStatusValueLabel.setText(value);
|
if (satelliteCountValueLabel != null) satelliteCountValueLabel.setText(value);
|
if (realtimeSpeedValueLabel != null) realtimeSpeedValueLabel.setText(value);
|
if (headingValueLabel != null) headingValueLabel.setText(value);
|
if (updateTimeValueLabel != null) updateTimeValueLabel.setText(value);
|
}
|
|
private String sanitizeDeviceValue(String value) {
|
if (value == null) {
|
return null;
|
}
|
String trimmed = value.trim();
|
if (trimmed.isEmpty() || "-1".equals(trimmed)) {
|
return null;
|
}
|
return trimmed;
|
}
|
|
private String formatDeviceValue(String value) {
|
String sanitized = sanitizeDeviceValue(value);
|
return sanitized == null ? "--" : sanitized;
|
}
|
|
private String formatTimestamp(String value) {
|
String sanitized = sanitizeDeviceValue(value);
|
if (sanitized == null) {
|
return "--";
|
}
|
try {
|
long millis = Long.parseLong(sanitized);
|
return TIMESTAMP_FORMAT.format(new Date(millis));
|
} catch (NumberFormatException ex) {
|
return sanitized;
|
}
|
}
|
|
private void handleBoundaryPointClick(Point screenPoint) {
|
if (currentBoundary == null || currentBoundaryLandNumber == null) {
|
return;
|
}
|
int hitIndex = findBoundaryPointIndex(screenPoint);
|
if (hitIndex < 0) {
|
return;
|
}
|
|
String pointLabel = String.valueOf(hitIndex + 1);
|
int choice = JOptionPane.showConfirmDialog(
|
visualizationPanel,
|
"确定要删除第" + pointLabel + "号边界点吗?",
|
"删除边界点",
|
JOptionPane.OK_CANCEL_OPTION,
|
JOptionPane.WARNING_MESSAGE
|
);
|
|
if (choice == JOptionPane.OK_OPTION) {
|
removeBoundaryPoint(hitIndex);
|
}
|
}
|
|
private int findBoundaryPointIndex(Point screenPoint) {
|
if (currentBoundary == null || currentBoundary.size() < 2) {
|
return -1;
|
}
|
boolean closed = isBoundaryClosed(currentBoundary);
|
int effectiveCount = closed ? currentBoundary.size() - 1 : currentBoundary.size();
|
if (effectiveCount <= 0) {
|
return -1;
|
}
|
|
double threshold = computeSelectionThresholdPixels();
|
|
for (int i = 0; i < effectiveCount; i++) {
|
Point2D.Double worldPoint = currentBoundary.get(i);
|
Point2D.Double screenPosition = worldToScreen(worldPoint);
|
double dx = screenPosition.x - screenPoint.x;
|
double dy = screenPosition.y - screenPoint.y;
|
if (Math.hypot(dx, dy) <= threshold) {
|
return i;
|
}
|
}
|
return -1;
|
}
|
|
private double computeSelectionThresholdPixels() {
|
double scaleFactor = Math.max(0.5, scale);
|
double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2);
|
double markerDiameterPixels = markerDiameterWorld * scale;
|
return Math.max(8.0, markerDiameterPixels * 1.5);
|
}
|
|
private Point2D.Double worldToScreen(Point2D.Double worldPoint) {
|
double screenX = (worldPoint.x + translateX) * scale + visualizationPanel.getWidth() / 2.0;
|
double screenY = (worldPoint.y + translateY) * scale + visualizationPanel.getHeight() / 2.0;
|
return new Point2D.Double(screenX, screenY);
|
}
|
|
private void removeBoundaryPoint(int index) {
|
if (currentBoundary == null || currentBoundary.size() < 2) {
|
return;
|
}
|
|
List<Point2D.Double> updated = new ArrayList<>(currentBoundary);
|
boolean closed = isBoundaryClosed(updated);
|
int effectiveCount = closed ? updated.size() - 1 : updated.size();
|
if (index < 0 || index >= effectiveCount) {
|
return;
|
}
|
|
updated.remove(index);
|
|
if (closed && updated.size() >= 2) {
|
Point2D.Double first = updated.get(0);
|
Point2D.Double last = updated.get(updated.size() - 1);
|
if (!arePointsClose(first, last)) {
|
updated.set(updated.size() - 1, new Point2D.Double(first.x, first.y));
|
}
|
}
|
|
boolean success = persistBoundaryChanges(updated);
|
if (!success) {
|
return;
|
}
|
|
if (updated.size() < 2) {
|
currentBoundary = null;
|
boundaryBounds = null;
|
boundaryPointsVisible = false;
|
Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, false);
|
visualizationPanel.repaint();
|
adjustViewAfterBoundaryReset();
|
} else {
|
currentBoundary = updated;
|
boundaryBounds = computeBounds(updated);
|
Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, boundaryPointsVisible);
|
visualizationPanel.repaint();
|
}
|
}
|
|
private boolean persistBoundaryChanges(List<Point2D.Double> updatedBoundary) {
|
if (currentBoundaryLandNumber == null) {
|
return false;
|
}
|
|
String serialized = serializeBoundary(updatedBoundary);
|
String storedValue = (serialized == null || serialized.trim().isEmpty()) ? "-1" : serialized;
|
|
boolean updated = Dikuai.updateField(currentBoundaryLandNumber, "boundaryCoordinates", storedValue);
|
if (!updated) {
|
JOptionPane.showMessageDialog(visualizationPanel, "无法更新边界数据", "错误", JOptionPane.ERROR_MESSAGE);
|
return false;
|
}
|
|
Dikuai.saveToProperties();
|
Dikuaiguanli.notifyExternalCreation(currentBoundaryLandNumber);
|
return true;
|
}
|
|
private String serializeBoundary(List<Point2D.Double> boundary) {
|
if (boundary == null || boundary.isEmpty()) {
|
return "";
|
}
|
StringBuilder builder = new StringBuilder();
|
for (int i = 0; i < boundary.size(); i++) {
|
Point2D.Double point = boundary.get(i);
|
builder.append(formatCoordinate(point.x))
|
.append(',')
|
.append(formatCoordinate(point.y));
|
if (i < boundary.size() - 1) {
|
builder.append(';');
|
}
|
}
|
return builder.toString();
|
}
|
|
private String formatCoordinate(double value) {
|
BigDecimal decimal = BigDecimal.valueOf(value).stripTrailingZeros();
|
return decimal.toPlainString();
|
}
|
|
private boolean isBoundaryClosed(List<Point2D.Double> boundary) {
|
if (boundary == null || boundary.size() < 2) {
|
return false;
|
}
|
Point2D.Double first = boundary.get(0);
|
Point2D.Double last = boundary.get(boundary.size() - 1);
|
return arePointsClose(first, last);
|
}
|
|
private boolean arePointsClose(Point2D.Double a, Point2D.Double b) {
|
if (a == null || b == null) {
|
return false;
|
}
|
double dx = a.x - b.x;
|
double dy = a.y - b.y;
|
return Math.hypot(dx, dy) <= BOUNDARY_POINT_MERGE_THRESHOLD;
|
}
|
|
|
/**
|
* 绘制视图信息
|
*/
|
private void drawViewInfo(Graphics2D g2d) {
|
g2d.setColor(new Color(80, 80, 80));
|
g2d.setFont(new Font("微软雅黑", Font.PLAIN, 11));
|
|
// 只显示缩放比例,去掉平移信息
|
String info = String.format("缩放: %.2fx", scale);
|
g2d.drawString(info, 15, visualizationPanel.getHeight() - 15);
|
|
if (mousePoint != null) {
|
// 计算鼠标位置在世界坐标系中的坐标
|
double worldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
|
double worldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
|
|
String mouseCoord = String.format("坐标: (%.1f, %.1f)m", worldX, worldY);
|
g2d.drawString(mouseCoord, visualizationPanel.getWidth() - 150, visualizationPanel.getHeight() - 15);
|
}
|
|
// 去掉操作提示:"滚轮缩放 | 拖拽移动 | 右键重置"
|
// String tips = "滚轮缩放 | 拖拽移动 | 右键重置";
|
// g2d.drawString(tips, visualizationPanel.getWidth() - 200, 30);
|
}
|
|
/**
|
* 获取当前缩放比例
|
*/
|
public double getScale() {
|
return scale;
|
}
|
|
/**
|
* 获取当前平移量X
|
*/
|
public double getTranslateX() {
|
return translateX;
|
}
|
|
/**
|
* 获取当前平移量Y
|
*/
|
public double getTranslateY() {
|
return translateY;
|
}
|
|
/**
|
* 设置视图变换参数(用于程序化控制)
|
*/
|
public void setViewTransform(double scale, double translateX, double translateY) {
|
this.scale = scale;
|
this.translateX = translateX;
|
this.translateY = translateY;
|
visualizationPanel.repaint();
|
}
|
|
public void setCurrentBoundary(String boundaryCoordinates, String landNumber, String landName) {
|
this.boundaryName = landName;
|
this.currentBoundaryLandNumber = landNumber;
|
|
if (boundaryCoordinates == null) {
|
clearBoundaryData();
|
adjustViewAfterBoundaryReset();
|
return;
|
}
|
|
String trimmed = boundaryCoordinates.trim();
|
if (trimmed.isEmpty() || "-1".equals(trimmed)) {
|
clearBoundaryData();
|
adjustViewAfterBoundaryReset();
|
return;
|
}
|
|
List<Point2D.Double> parsed = parseBoundary(trimmed);
|
if (parsed.size() < 2) {
|
clearBoundaryData();
|
adjustViewAfterBoundaryReset();
|
return;
|
}
|
|
currentBoundary = parsed;
|
boundaryBounds = computeBounds(parsed);
|
|
Rectangle2D.Double bounds = boundaryBounds;
|
SwingUtilities.invokeLater(() -> {
|
fitBoundsToView(bounds);
|
visualizationPanel.repaint();
|
});
|
}
|
|
private void clearBoundaryData() {
|
currentBoundary = null;
|
boundaryBounds = null;
|
boundaryName = null;
|
boundaryPointsVisible = false;
|
currentBoundaryLandNumber = null;
|
}
|
|
public void setCurrentObstacles(String obstaclesData, String landNumber) {
|
if (landNumber == null) {
|
clearObstacleData();
|
if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
|
resetView();
|
} else {
|
visualizationPanel.repaint();
|
}
|
return;
|
}
|
|
List<Obstacledge.Obstacle> parsed = parseObstacles(obstaclesData, landNumber);
|
if (parsed.isEmpty()) {
|
clearObstacleData();
|
if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
|
resetView();
|
} else {
|
visualizationPanel.repaint();
|
}
|
return;
|
}
|
|
currentObstacles = parsed;
|
obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(parsed));
|
selectedObstacleName = null;
|
|
if (!hasRenderableBoundary() && !hasRenderablePlannedPath() && obstacleBounds != null) {
|
Rectangle2D.Double bounds = obstacleBounds;
|
SwingUtilities.invokeLater(() -> {
|
fitBoundsToView(bounds);
|
visualizationPanel.repaint();
|
});
|
} else {
|
visualizationPanel.repaint();
|
}
|
}
|
|
private void clearObstacleData() {
|
currentObstacles = null;
|
obstacleBounds = null;
|
selectedObstacleName = null;
|
}
|
|
private List<Obstacledge.Obstacle> parseObstacles(String obstaclesData, String landNumber) {
|
List<Obstacledge.Obstacle> obstacles = new ArrayList<>();
|
if (obstaclesData == null) {
|
return obstacles;
|
}
|
|
String normalized = stripInlineComment(obstaclesData.trim());
|
if (normalized.isEmpty() || "-1".equals(normalized)) {
|
return obstacles;
|
}
|
|
List<String> entries = splitObstacleEntries(normalized);
|
int defaultIndex = 1;
|
|
for (String entry : entries) {
|
String trimmedEntry = stripInlineComment(entry);
|
if (trimmedEntry.isEmpty()) {
|
continue;
|
}
|
|
String nameToken = null;
|
String shapeToken = null;
|
String coordsSection = trimmedEntry;
|
|
if (trimmedEntry.contains("::")) {
|
String[] parts = trimmedEntry.split("::", 3);
|
if (parts.length == 3) {
|
nameToken = parts[0].trim();
|
shapeToken = parts[1].trim();
|
coordsSection = parts[2].trim();
|
}
|
} else if (trimmedEntry.contains("@")) {
|
String[] parts = trimmedEntry.split("@", 3);
|
if (parts.length == 3) {
|
nameToken = parts[0].trim();
|
shapeToken = parts[1].trim();
|
coordsSection = parts[2].trim();
|
} else if (parts.length == 2) {
|
shapeToken = parts[0].trim();
|
coordsSection = parts[1].trim();
|
}
|
} else if (trimmedEntry.contains(":")) {
|
String[] parts = trimmedEntry.split(":", 3);
|
if (parts.length == 3) {
|
nameToken = parts[0].trim();
|
shapeToken = parts[1].trim();
|
coordsSection = parts[2].trim();
|
} else if (parts.length == 2) {
|
if (looksLikeShapeToken(parts[0])) {
|
shapeToken = parts[0].trim();
|
coordsSection = parts[1].trim();
|
} else {
|
nameToken = parts[0].trim();
|
coordsSection = parts[1].trim();
|
}
|
}
|
}
|
|
List<Obstacledge.XYCoordinate> xyCoordinates = parseObstacleCoordinates(coordsSection);
|
if (xyCoordinates.size() < 2) {
|
continue;
|
}
|
|
Obstacledge.ObstacleShape shape = resolveObstacleShape(shapeToken, xyCoordinates.size());
|
if (shape == null) {
|
continue;
|
}
|
|
String obstacleName = (nameToken != null && !nameToken.isEmpty())
|
? nameToken
|
: "障碍物" + defaultIndex++;
|
|
Obstacledge.Obstacle obstacle = new Obstacledge.Obstacle(landNumber, obstacleName, shape);
|
obstacle.setXyCoordinates(new ArrayList<>(xyCoordinates));
|
populateDummyOriginalCoordinates(obstacle, xyCoordinates.size());
|
|
if (obstacle.isValid()) {
|
obstacles.add(obstacle);
|
}
|
}
|
|
return obstacles;
|
}
|
|
private boolean looksLikeShapeToken(String token) {
|
if (token == null) {
|
return false;
|
}
|
String normalized = token.trim().toLowerCase(Locale.ROOT);
|
return "circle".equals(normalized)
|
|| "polygon".equals(normalized)
|
|| "圆形".equals(normalized)
|
|| "多边形".equals(normalized)
|
|| "0".equals(normalized)
|
|| "1".equals(normalized);
|
}
|
|
private List<Obstacledge.XYCoordinate> parseObstacleCoordinates(String coordsSection) {
|
List<Obstacledge.XYCoordinate> coords = new ArrayList<>();
|
if (coordsSection == null) {
|
return coords;
|
}
|
|
String sanitized = stripInlineComment(coordsSection.trim());
|
if (sanitized.isEmpty() || "-1".equals(sanitized)) {
|
return coords;
|
}
|
|
String[] pairs = sanitized.split(";");
|
for (String pair : pairs) {
|
if (pair == null) {
|
continue;
|
}
|
String trimmed = stripInlineComment(pair.trim());
|
if (trimmed.isEmpty()) {
|
continue;
|
}
|
String[] parts = trimmed.split(",");
|
if (parts.length < 2) {
|
continue;
|
}
|
try {
|
double x = Double.parseDouble(parts[0].trim());
|
double y = Double.parseDouble(parts[1].trim());
|
coords.add(new Obstacledge.XYCoordinate(x, y));
|
} catch (NumberFormatException ignored) {
|
// Skip malformed coordinate pair
|
}
|
}
|
|
return coords;
|
}
|
|
private Obstacledge.ObstacleShape resolveObstacleShape(String shapeToken, int coordinateCount) {
|
if (shapeToken != null && !shapeToken.trim().isEmpty()) {
|
String normalized = shapeToken.trim().toLowerCase(Locale.ROOT);
|
if ("circle".equals(normalized) || "圆形".equals(normalized) || "0".equals(normalized)) {
|
return Obstacledge.ObstacleShape.CIRCLE;
|
}
|
if ("polygon".equals(normalized) || "多边形".equals(normalized) || "1".equals(normalized)) {
|
return Obstacledge.ObstacleShape.POLYGON;
|
}
|
}
|
|
if (coordinateCount == 2) {
|
return Obstacledge.ObstacleShape.CIRCLE;
|
}
|
if (coordinateCount >= 3) {
|
return Obstacledge.ObstacleShape.POLYGON;
|
}
|
return null;
|
}
|
|
private void populateDummyOriginalCoordinates(Obstacledge.Obstacle obstacle, int xyCount) {
|
List<Obstacledge.DMCoordinate> dmCoordinates = new ArrayList<>();
|
int points = Math.max(1, xyCount);
|
for (int i = 0; i < points; i++) {
|
dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'N'));
|
dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'E'));
|
}
|
obstacle.setOriginalCoordinates(dmCoordinates);
|
}
|
|
private List<String> splitObstacleEntries(String data) {
|
List<String> entries = new ArrayList<>();
|
if (data.indexOf('|') >= 0) {
|
String[] parts = data.split("\\|");
|
for (String part : parts) {
|
if (part != null && !part.trim().isEmpty()) {
|
entries.add(part.trim());
|
}
|
}
|
} else if (data.contains("\n")) {
|
String[] lines = data.split("\r?\n");
|
for (String line : lines) {
|
if (line != null && !line.trim().isEmpty()) {
|
entries.add(line.trim());
|
}
|
}
|
} else {
|
entries.add(data);
|
}
|
return entries;
|
}
|
|
private String stripInlineComment(String text) {
|
if (text == null) {
|
return "";
|
}
|
int hashIndex = text.indexOf('#');
|
if (hashIndex >= 0) {
|
return text.substring(0, hashIndex).trim();
|
}
|
return text.trim();
|
}
|
|
private Rectangle2D.Double convertObstacleBounds(double[] bounds) {
|
if (bounds == null || bounds.length < 4) {
|
return null;
|
}
|
double minX = bounds[0];
|
double minY = bounds[1];
|
double maxX = bounds[2];
|
double maxY = bounds[3];
|
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
}
|
|
private boolean hasRenderableBoundary() {
|
return currentBoundary != null && currentBoundary.size() >= 2;
|
}
|
|
private boolean hasRenderablePlannedPath() {
|
return currentPlannedPath != null && currentPlannedPath.size() >= 2;
|
}
|
|
private void adjustViewAfterBoundaryReset() {
|
if (plannedPathBounds != null) {
|
Rectangle2D.Double bounds = plannedPathBounds;
|
SwingUtilities.invokeLater(() -> {
|
fitBoundsToView(bounds);
|
visualizationPanel.repaint();
|
});
|
return;
|
}
|
|
if (obstacleBounds != null) {
|
Rectangle2D.Double bounds = obstacleBounds;
|
SwingUtilities.invokeLater(() -> {
|
fitBoundsToView(bounds);
|
visualizationPanel.repaint();
|
});
|
return;
|
}
|
|
resetView();
|
}
|
|
public void setCurrentPlannedPath(String plannedPath) {
|
if (plannedPath == null) {
|
currentPlannedPath = null;
|
plannedPathBounds = null;
|
if (!hasRenderableBoundary()) {
|
resetView();
|
} else {
|
visualizationPanel.repaint();
|
}
|
return;
|
}
|
|
List<Point2D.Double> parsed = lujingdraw.parsePlannedPath(plannedPath);
|
if (parsed.size() < 2) {
|
currentPlannedPath = null;
|
plannedPathBounds = null;
|
if (!hasRenderableBoundary()) {
|
resetView();
|
} else {
|
visualizationPanel.repaint();
|
}
|
return;
|
}
|
|
currentPlannedPath = parsed;
|
plannedPathBounds = computeBounds(parsed);
|
|
Rectangle2D.Double bounds = plannedPathBounds;
|
SwingUtilities.invokeLater(() -> {
|
if (!hasRenderableBoundary()) {
|
fitBoundsToView(bounds);
|
}
|
visualizationPanel.repaint();
|
});
|
}
|
|
public void setBoundaryPointsVisible(boolean visible) {
|
this.boundaryPointsVisible = visible;
|
visualizationPanel.repaint();
|
}
|
|
private List<Point2D.Double> parseBoundary(String boundaryCoordinates) {
|
List<Point2D.Double> points = new ArrayList<>();
|
String[] entries = boundaryCoordinates.split(";");
|
|
for (String entry : entries) {
|
if (entry == null || entry.trim().isEmpty()) {
|
continue;
|
}
|
String[] parts = entry.trim().split(",");
|
if (parts.length < 2) {
|
continue;
|
}
|
try {
|
double x = Double.parseDouble(parts[0].trim());
|
double y = Double.parseDouble(parts[1].trim());
|
points.add(new Point2D.Double(x, y));
|
} catch (NumberFormatException ex) {
|
// ignore invalid entries
|
}
|
}
|
return points;
|
}
|
|
private Rectangle2D.Double computeBounds(List<Point2D.Double> points) {
|
double minX = Double.MAX_VALUE;
|
double minY = Double.MAX_VALUE;
|
double maxX = -Double.MAX_VALUE;
|
double maxY = -Double.MAX_VALUE;
|
|
for (Point2D.Double point : points) {
|
if (point.x < minX) minX = point.x;
|
if (point.x > maxX) maxX = point.x;
|
if (point.y < minY) minY = point.y;
|
if (point.y > maxY) maxY = point.y;
|
}
|
|
if (minX == Double.MAX_VALUE) {
|
return null;
|
}
|
|
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
}
|
|
private void fitBoundsToView(Rectangle2D.Double bounds) {
|
if (bounds == null || visualizationPanel.getWidth() <= 0 || visualizationPanel.getHeight() <= 0) {
|
return;
|
}
|
|
double width = Math.max(bounds.width, 1);
|
double height = Math.max(bounds.height, 1);
|
|
double targetWidth = width * 1.2;
|
double targetHeight = height * 1.2;
|
|
double panelWidth = visualizationPanel.getWidth();
|
double panelHeight = visualizationPanel.getHeight();
|
|
double newScale = Math.min(panelWidth / targetWidth, panelHeight / targetHeight);
|
newScale = Math.max(0.05, Math.min(newScale, 50.0));
|
|
this.scale = newScale;
|
this.translateX = -bounds.getCenterX();
|
this.translateY = -bounds.getCenterY();
|
}
|
|
public void dispose() {
|
mowerUpdateTimer.stop();
|
stopMowerInfoTimer();
|
if (mowerInfoDialog != null) {
|
mowerInfoDialog.dispose();
|
mowerInfoDialog = null;
|
}
|
resetMowerInfoLabels();
|
}
|
|
}
|