package gecaoji;
|
|
import java.awt.Color;
|
import java.awt.Graphics2D;
|
import java.awt.Image;
|
import java.awt.geom.AffineTransform;
|
import java.awt.geom.Ellipse2D;
|
import java.awt.geom.Point2D;
|
|
import javax.swing.ImageIcon;
|
|
/** 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 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() {
|
defaultIcon = loadIcon(DEFAULT_ICON_PATH);
|
handheldIcon = loadIcon(HANDHELD_ICON_PATH);
|
activeIcon = defaultIcon != null ? defaultIcon : handheldIcon;
|
handheldIconActive = false;
|
refreshFromDevice();
|
}
|
|
private Image loadIcon(String path) {
|
try {
|
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 from " + path + ": " + ex.getMessage());
|
return null;
|
}
|
}
|
|
public void refreshFromDevice() {
|
Device device = Device.getGecaoji();
|
if (device == null) {
|
positionValid = false;
|
return;
|
}
|
|
double x = parseCoordinate(device.getRealtimeX());
|
double y = parseCoordinate(device.getRealtimeY());
|
double heading = parseHeading(device.getHeading());
|
if (Double.isNaN(x) || Double.isNaN(y)) {
|
// Keep showing the last known mower position when temporary sensor glitches occur.
|
return;
|
}
|
|
ensurePosition();
|
position.x = x;
|
position.y = y;
|
positionValid = true;
|
headingDegrees = heading;
|
}
|
|
private void ensurePosition() {
|
if (position == null) {
|
position = new Point2D.Double();
|
}
|
}
|
|
private double parseCoordinate(String value) {
|
if (value == null) {
|
return Double.NaN;
|
}
|
String trimmed = value.trim();
|
if (trimmed.isEmpty() || "-1".equals(trimmed)) {
|
return Double.NaN;
|
}
|
try {
|
return Double.parseDouble(trimmed);
|
} catch (NumberFormatException ex) {
|
return Double.NaN;
|
}
|
}
|
|
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 * 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, 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(icon, 0, 0, null);
|
g2d.setTransform(original);
|
}
|
|
private void drawFallback(Graphics2D g2d, double worldSize) {
|
double radius = worldSize / 2.0;
|
ensurePosition();
|
fallbackShape.setFrame(position.x - radius, position.y - radius, worldSize, worldSize);
|
|
Color original = g2d.getColor();
|
g2d.setColor(FALLBACK_FILL);
|
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);
|
}
|
|
public boolean hasValidPosition() {
|
return positionValid;
|
}
|
|
public Point2D.Double getPosition() {
|
if (!positionValid) {
|
return null;
|
}
|
ensurePosition();
|
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 * 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;
|
}
|
}
|