| | |
| | | /** 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 final Ellipse2D.Double fallbackShape = new Ellipse2D.Double(); |
| | | private Point2D.Double position = new Point2D.Double(); |
| | | private boolean positionValid; |
| | | private double headingDegrees; |
| | | |
| | | public Gecaoji() { |
| | | mowerIcon = loadIcon(); |
| | |
| | | |
| | | private Image loadIcon() { |
| | | try { |
| | | ImageIcon icon = new ImageIcon("image/mow.png"); |
| | | ImageIcon icon = new ImageIcon("image/gecaoji.png"); |
| | | if (icon.getIconWidth() <= 0 || icon.getIconHeight() <= 0) { |
| | | return null; |
| | | } |
| | |
| | | |
| | | double x = parseCoordinate(device.getRealtimeX()); |
| | | double y = parseCoordinate(device.getRealtimeY()); |
| | | double heading = parseHeading(device.getHeading()); |
| | | if (Double.isNaN(x) || Double.isNaN(y)) { |
| | | positionValid = false; |
| | | return; |
| | |
| | | position.x = x; |
| | | position.y = y; |
| | | positionValid = true; |
| | | headingDegrees = heading; |
| | | } |
| | | |
| | | 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); |
| | | double worldSize = (ICON_PIXEL_SIZE * ICON_SCALE_FACTOR) / Math.max(scale, MIN_SCALE); |
| | | if (mowerIcon != null && mowerIcon.getWidth(null) > 0 && mowerIcon.getHeight(null) > 0) { |
| | | drawIcon(g2d, worldSize); |
| | | } else { |
| | |
| | | double iconHeight = mowerIcon.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.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); |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |