张世豪
8 小时以前 ed6936545d20cc490145d2936cee4387be2afd53
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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() {
        // 检查是否正在导航预览模式,如果是则不更新位置
        if (isNavigating()) {
            return;
        }
        
        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;
    }
    
    /**
     * 检查是否正在导航预览模式
     * @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() {
        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;
    }
}