| | |
| | | /** Mower rendering helper. */ |
| | | public class Gecaoji { |
| | | private static final double ICON_PIXEL_SIZE = 48.0; |
| | | private static final double ICON_SCALE_FACTOR = 0.8; |
| | | private static final double MIN_SCALE = 1e-6; |
| | | private static final Color FALLBACK_FILL = new Color(0, 150, 0); |
| | | private static final String DEFAULT_ICON_PATH = "image/gecaoji.png"; |
| | | private static final String HANDHELD_ICON_PATH = "image/URT.png"; |
| | | |
| | | private final Image mowerIcon; |
| | | private final Image defaultIcon; |
| | | private final Image handheldIcon; |
| | | private Image activeIcon; |
| | | private boolean handheldIconActive; |
| | | private final Ellipse2D.Double fallbackShape = new Ellipse2D.Double(); |
| | | private Point2D.Double position = new Point2D.Double(); |
| | | private boolean positionValid; |
| | | private double headingDegrees; |
| | | |
| | | public Gecaoji() { |
| | | mowerIcon = loadIcon(); |
| | | defaultIcon = loadIcon(DEFAULT_ICON_PATH); |
| | | handheldIcon = loadIcon(HANDHELD_ICON_PATH); |
| | | activeIcon = defaultIcon != null ? defaultIcon : handheldIcon; |
| | | handheldIconActive = false; |
| | | refreshFromDevice(); |
| | | } |
| | | |
| | | private Image loadIcon() { |
| | | private Image loadIcon(String path) { |
| | | try { |
| | | ImageIcon icon = new ImageIcon("image/mow.png"); |
| | | ImageIcon icon = new ImageIcon(path); |
| | | if (icon.getIconWidth() <= 0 || icon.getIconHeight() <= 0) { |
| | | return null; |
| | | } |
| | | return icon.getImage(); |
| | | } catch (Exception ex) { |
| | | System.err.println("Unable to load mower icon: " + ex.getMessage()); |
| | | System.err.println("Unable to load mower icon from " + path + ": " + ex.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public void refreshFromDevice() { |
| | | // 检查是否正在导航预览模式,如果是则不更新位置 |
| | | if (isNavigating()) { |
| | | return; |
| | | } |
| | | |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | positionValid = false; |
| | |
| | | |
| | | double x = parseCoordinate(device.getRealtimeX()); |
| | | double y = parseCoordinate(device.getRealtimeY()); |
| | | double heading = parseHeading(device.getHeading()); |
| | | if (Double.isNaN(x) || Double.isNaN(y)) { |
| | | positionValid = false; |
| | | // Keep showing the last known mower position when temporary sensor glitches occur. |
| | | return; |
| | | } |
| | | |
| | |
| | | position.x = x; |
| | | position.y = y; |
| | | positionValid = true; |
| | | headingDegrees = heading; |
| | | } |
| | | |
| | | /** |
| | | * 检查是否正在导航预览模式 |
| | | * @return 如果正在导航预览返回true,否则返回false |
| | | */ |
| | | private boolean isNavigating() { |
| | | try { |
| | | dikuai.daohangyulan nav = dikuai.daohangyulan.getInstance(); |
| | | if (nav != null) { |
| | | return nav.isNavigating(); |
| | | } |
| | | } catch (Exception e) { |
| | | // 如果获取导航实例失败,返回false(不影响主要功能) |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private void ensurePosition() { |
| | |
| | | } |
| | | } |
| | | |
| | | private double parseHeading(String value) { |
| | | if (value == null) { |
| | | return 0.0; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double parsed = Double.parseDouble(trimmed); |
| | | if (!Double.isFinite(parsed)) { |
| | | return 0.0; |
| | | } |
| | | double normalized = parsed % 360.0; |
| | | return normalized < 0 ? normalized + 360.0 : normalized; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | public void draw(Graphics2D g2d, double scale) { |
| | | if (!positionValid) { |
| | | return; |
| | | } |
| | | |
| | | double worldSize = ICON_PIXEL_SIZE / Math.max(scale, MIN_SCALE); |
| | | if (mowerIcon != null && mowerIcon.getWidth(null) > 0 && mowerIcon.getHeight(null) > 0) { |
| | | drawIcon(g2d, worldSize); |
| | | double worldSize = (ICON_PIXEL_SIZE * ICON_SCALE_FACTOR) / Math.max(scale, MIN_SCALE); |
| | | Image icon = activeIcon; |
| | | if (icon != null && icon.getWidth(null) > 0 && icon.getHeight(null) > 0) { |
| | | drawIcon(g2d, worldSize, icon); |
| | | } else { |
| | | drawFallback(g2d, worldSize); |
| | | } |
| | | } |
| | | |
| | | private void drawIcon(Graphics2D g2d, double worldSize) { |
| | | double iconWidth = mowerIcon.getWidth(null); |
| | | double iconHeight = mowerIcon.getHeight(null); |
| | | private void drawIcon(Graphics2D g2d, double worldSize, Image icon) { |
| | | double iconWidth = icon.getWidth(null); |
| | | double iconHeight = icon.getHeight(null); |
| | | double maxSide = Math.max(iconWidth, iconHeight); |
| | | double scaleFactor = worldSize / Math.max(maxSide, MIN_SCALE); |
| | | double rotationRadians = Math.toRadians(-headingDegrees); |
| | | |
| | | AffineTransform original = g2d.getTransform(); |
| | | AffineTransform transformed = new AffineTransform(original); |
| | | transformed.translate(position.x, position.y); |
| | | if (rotationRadians != 0.0) { |
| | | transformed.rotate(rotationRadians); |
| | | } |
| | | transformed.scale(scaleFactor, scaleFactor); |
| | | transformed.translate(-iconWidth / 2.0, -iconHeight / 2.0); |
| | | g2d.setTransform(transformed); |
| | | g2d.drawImage(mowerIcon, 0, 0, null); |
| | | g2d.drawImage(icon, 0, 0, null); |
| | | g2d.setTransform(original); |
| | | } |
| | | |
| | |
| | | g2d.fill(fallbackShape); |
| | | g2d.setColor(Color.WHITE); |
| | | g2d.draw(fallbackShape); |
| | | double rotationRadians = Math.toRadians(-headingDegrees); |
| | | double lineLength = radius; |
| | | double dx = lineLength * Math.sin(rotationRadians); |
| | | double dy = lineLength * Math.cos(rotationRadians); |
| | | g2d.drawLine( |
| | | (int) Math.round(position.x), |
| | | (int) Math.round(position.y), |
| | | (int) Math.round(position.x + dx), |
| | | (int) Math.round(position.y + dy)); |
| | | g2d.setColor(original); |
| | | } |
| | | |
| | |
| | | return new Point2D.Double(position.x, position.y); |
| | | } |
| | | |
| | | /** |
| | | * 设置割草机位置(用于导航预览等场景) |
| | | * @param x X坐标 |
| | | * @param y Y坐标 |
| | | */ |
| | | public void setPosition(double x, double y) { |
| | | ensurePosition(); |
| | | position.x = x; |
| | | position.y = y; |
| | | positionValid = true; |
| | | } |
| | | |
| | | /** |
| | | * 设置割草机方向(用于导航预览等场景) |
| | | * @param headingDegrees 方向角度(度,0-360) |
| | | */ |
| | | public void setHeading(double headingDegrees) { |
| | | double normalized = headingDegrees % 360.0; |
| | | if (normalized < 0) { |
| | | normalized += 360.0; |
| | | } |
| | | this.headingDegrees = normalized; |
| | | } |
| | | |
| | | /** |
| | | * 获取割草机方向 |
| | | * @return 方向角度(度,0-360) |
| | | */ |
| | | public double getHeading() { |
| | | return headingDegrees; |
| | | } |
| | | |
| | | public double getWorldRadius(double scale) { |
| | | if (!positionValid) { |
| | | return Double.NaN; |
| | | } |
| | | double worldSize = ICON_PIXEL_SIZE / Math.max(scale, MIN_SCALE); |
| | | double worldSize = (ICON_PIXEL_SIZE * ICON_SCALE_FACTOR) / Math.max(scale, MIN_SCALE); |
| | | return worldSize / 2.0; |
| | | } |
| | | |
| | | public boolean useHandheldIcon(boolean handheldMode) { |
| | | if (handheldIconActive == handheldMode) { |
| | | return false; |
| | | } |
| | | handheldIconActive = handheldMode; |
| | | Image next = handheldMode |
| | | ? (handheldIcon != null ? handheldIcon : defaultIcon) |
| | | : defaultIcon; |
| | | if (next == null) { |
| | | next = handheldIcon; |
| | | } |
| | | activeIcon = next; |
| | | return true; |
| | | } |
| | | } |