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 gecaoji.Device; import gecaoji.Gecaoji; import dikuai.Dikuaiguanli; import dikuai.Dikuai; /** * 地图渲染器 - 负责坐标系绘制、视图变换等功能 */ 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 currentBoundary; private Rectangle2D.Double boundaryBounds; private List currentPlannedPath; private Rectangle2D.Double plannedPathBounds; 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; if (hasBoundary) { drawCurrentBoundary(g2d); } 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 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 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 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 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 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; } private void adjustViewAfterBoundaryReset() { if (plannedPathBounds != null) { Rectangle2D.Double bounds = plannedPathBounds; SwingUtilities.invokeLater(() -> { fitBoundsToView(bounds); visualizationPanel.repaint(); }); } else { resetView(); } } public void setCurrentPlannedPath(String plannedPath) { if (plannedPath == null) { currentPlannedPath = null; plannedPathBounds = null; if (currentBoundary == null || currentBoundary.size() < 2) { resetView(); } else { visualizationPanel.repaint(); } return; } List parsed = lujingdraw.parsePlannedPath(plannedPath); if (parsed.size() < 2) { currentPlannedPath = null; plannedPathBounds = null; if (currentBoundary == null || currentBoundary.size() < 2) { resetView(); } else { visualizationPanel.repaint(); } return; } currentPlannedPath = parsed; plannedPathBounds = computeBounds(parsed); Rectangle2D.Double bounds = plannedPathBounds; SwingUtilities.invokeLater(() -> { if (currentBoundary == null || currentBoundary.size() < 2) { fitBoundsToView(bounds); } visualizationPanel.repaint(); }); } public void setBoundaryPointsVisible(boolean visible) { this.boundaryPointsVisible = visible; visualizationPanel.repaint(); } private List parseBoundary(String boundaryCoordinates) { List 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 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(); } }