张世豪
2025-12-09 32524195d474b74e48916867b2a6c2f022a40d98
20251209
已添加4个文件
已修改25个文件
已删除3个文件
3347 ■■■■ 文件已修改
Obstacledge.properties 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basestation.properties 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
homein.lock 补丁 | 查看 | 原始文档 | blame | 历史
image/URT.png 补丁 | 查看 | 原始文档 | blame | 历史
lib/native/windows/x86_64/jSerialComm.dll 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/TestPlanner.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/baseStation/BaseStationDialog.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/BoundaryAlgorithm.java 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/bianjieguihua.java 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/bianjieguihua2.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/chuankou/SerialPortNativeLoader.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/chuankou/SerialPortService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/denglu/Denglu.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/denglu/RegistrationFrame.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/addzhangaiwu.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/Gecaoji.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/lujingdraw.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/homein/Homein.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/Lunjingguihua.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/luoxuan.java 408 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/set/Sets.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 392 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/HandheldBoundaryCaptureDialog.java 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 755 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/adddikuaiyulan.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/pointandnumber.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/zijian.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
user.properties 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,10 +1,11 @@
# å‰²è‰æœºåœ°å—障碍物配置文件
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-05T15:37:09.106292500
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-09T11:53:37.128295200
# åæ ‡ç³»ï¼šWGS84(度分格式)
# ============ åœ°å—基准站配置 ============
# æ ¼å¼ï¼šplot.[地块编号].baseStation=[经度],[N/S],[纬度],[E/W]
plot.DK-001.baseStation=3949.902389,N,11616.756920,E
plot.LAND1.baseStation=3949.902389,N,11616.756920,E
# ============ éšœç¢ç‰©é…ç½® ============
# æ ¼å¼ï¼šplot.[地块编号].obstacle.[障碍物名称].shape=[0|1]
basestation.properties
@@ -1,7 +1,7 @@
#Base station properties
#Sat Nov 29 15:05:51 CST 2025
#Tue Dec 09 19:03:31 CST 2025
dataUpdateTime=-1
deviceActivationTime=-1
deviceId=1234
deviceId=4567
installationCoordinates=3949.90238860,N,11616.75692000,E
iotSimCardNumber=-1
dikuai.properties
@@ -1,19 +1,19 @@
#Dikuai Properties
#Fri Dec 05 19:16:23 CST 2025
DK-001.angleThreshold=-1
DK-001.baseStationCoordinates=3949.90238860,N,11616.75692000,E
DK-001.boundaryCoordinates=-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88
DK-001.boundaryOriginalCoordinates=-1
DK-001.boundaryPointInterval=-1
DK-001.createTime=-1
DK-001.intelligentSceneAnalysis=-1
DK-001.landArea=300
DK-001.landName=前院草坪
DK-001.landNumber=DK-001
DK-001.mowingPattern=parallel
DK-001.mowingTrack=-1
DK-001.mowingWidth=100
DK-001.plannedPath=1.51,-8.35;0.26,-1.43;-1.35,1.89;0.77,-9.89;-0.22,-10.06;-2.33,1.68
DK-001.returnPointCoordinates=-1
DK-001.updateTime=2025-12-05 19\:16\:23
DK-001.userId=-1
#Tue Dec 09 11:53:37 CST 2025
LAND1.angleThreshold=-1
LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E
LAND1.boundaryCoordinates=1.31,-9.59;1.86,-11.61;3.12,-12.49;5.50,-12.06;5.95,-10.88;4.97,-3.86;3.16,-0.87;2.79,-2.61;2.42,-4.35;2.05,-6.10;1.68,-7.84;1.31,-9.59
LAND1.boundaryOriginalCoordinates=39.831620,116.279297,39.70;39.831618,116.279298,39.80;39.831616,116.279298,39.70;39.831614,116.279298,39.70;39.831612,116.279299,39.70;39.831610,116.279299,39.70;39.831608,116.279300,39.70;39.831606,116.279301,39.70;39.831604,116.279303,39.70;39.831602,116.279304,39.70;39.831600,116.279306,39.70;39.831598,116.279307,39.70;39.831598,116.279309,39.70;39.831597,116.279312,39.60;39.831596,116.279314,39.60;39.831595,116.279317,39.60;39.831594,116.279319,39.60;39.831594,116.279320,39.60;39.831594,116.279323,39.60;39.831595,116.279326,39.60;39.831595,116.279329,39.60;39.831595,116.279331,39.70;39.831595,116.279334,39.70;39.831596,116.279337,39.60;39.831596,116.279339,39.60;39.831596,116.279342,39.60;39.831597,116.279344,39.60;39.831598,116.279346,39.70;39.831600,116.279348,39.70;39.831601,116.279349,39.70;39.831603,116.279350,39.80;39.831605,116.279350,39.70;39.831606,116.279351,39.70;39.831609,116.279352,39.80;39.831611,116.279352,39.70;39.831613,116.279352,39.70;39.831615,116.279352,39.70;39.831617,116.279353,39.70;39.831619,116.279353,39.70;39.831621,116.279353,39.80;39.831623,116.279353,39.80;39.831625,116.279353,39.80;39.831627,116.279353,39.80;39.831629,116.279352,39.80;39.831631,116.279352,39.80;39.831634,116.279351,39.70;39.831636,116.279351,39.70;39.831637,116.279350,39.70;39.831639,116.279350,39.80;39.831641,116.279349,39.70;39.831643,116.279348,39.70;39.831645,116.279348,39.70;39.831647,116.279347,39.70;39.831649,116.279346,39.80;39.831651,116.279346,39.80;39.831653,116.279345,39.80;39.831655,116.279345,39.80;39.831657,116.279344,39.80;39.831659,116.279343,39.70;39.831661,116.279343,39.70;39.831663,116.279342,39.70;39.831665,116.279342,39.70;39.831667,116.279341,39.70;39.831670,116.279341,39.70;39.831672,116.279340,39.70;39.831674,116.279339,39.80;39.831676,116.279338,39.80;39.831678,116.279337,39.80;39.831679,116.279336,39.70;39.831680,116.279334,39.70;39.831681,116.279332,39.70;39.831683,116.279331,39.70;39.831684,116.279329,39.70;39.831686,116.279327,39.70;39.831687,116.279325,39.70;39.831689,116.279323,39.70;39.831691,116.279322,39.70;39.831693,116.279321,39.70;39.831694,116.279320,39.70;39.831696,116.279319,39.70;39.831699,116.279319,39.70
LAND1.boundaryPointInterval=-1
LAND1.createTime=2025-12-09 11\:16\:40
LAND1.intelligentSceneAnalysis=-1
LAND1.landArea=35.87
LAND1.landName=1234
LAND1.landNumber=LAND1
LAND1.mowingPattern=螺旋式
LAND1.mowingTrack=-1
LAND1.mowingWidth=40
LAND1.plannedPath=1.88,-7.88;2.62,-4.39;3.25,-1.41;4.78,-3.93;5.74,-10.86;5.35,-11.88;3.17,-12.28;2.03,-11.49;1.52,-9.58;1.88,-7.88;2.27,-7.96;3.01,-4.47;3.43,-2.48;4.39,-4.07;5.33,-10.81;5.06,-11.53;3.26,-11.86;2.38,-11.24;1.93,-9.57;2.27,-7.96;2.66,-8.05;3.40,-4.56;3.61,-3.55;4.01,-4.20;4.92,-10.76;4.77,-11.18;3.35,-11.43;2.73,-11.00;2.34,-9.56;2.66,-8.05;3.05,-8.13;3.71,-4.99;4.51,-10.72;4.47,-10.82;3.44,-11.01;3.08,-10.75;2.75,-9.55;3.05,-8.13;3.44,-8.21;3.63,-7.30;4.08,-10.49;3.54,-10.59;3.43,-10.51;3.16,-9.54;3.44,-8.21
LAND1.returnPointCoordinates=-1
LAND1.updateTime=2025-12-09 11\:53\:37
LAND1.userId=-1
homein.lock
image/URT.png

lib/native/windows/x86_64/jSerialComm.dll
Binary files differ
set.properties
@@ -1,11 +1,12 @@
#Mower Configuration Properties - Updated
#Fri Dec 05 14:26:38 CST 2025
#Current work land selection updated
#Tue Dec 09 19:35:30 CST 2025
appVersion=-1
currentWorkLandNumber=LAND1
cuttingWidth=200
firmwareVersion=-1
handheldMarkerId=2354
handheldMarkerId=
idleTrailDurationSeconds=60
mowerId=1872
mowerId=1234
serialAutoConnect=true
serialBaudRate=921600
serialPortName=COM15
src/TestPlanner.java
@@ -2,10 +2,10 @@
public class TestPlanner {
    public static void main(String[] args) {
        String polygon = "-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88";
    String polygon = "-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88";
        String obstacles = "";
        String widthMeters = "0.5";
        String mode = "parallel";
    String widthMeters = "0.5";
    String mode = "parallel";
        String pathCoords = Lunjingguihua.generatePathFromStrings(polygon, obstacles, widthMeters, mode);
        System.out.println(pathCoords);
src/baseStation/BaseStationDialog.java
@@ -13,6 +13,7 @@
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Properties;
public class BaseStationDialog extends JDialog {
@@ -180,26 +181,24 @@
        baseStation.load();
        String currentRaw = baseStation.getInstallationCoordinates();
        String normalizedCurrent = currentRaw == null ? "-1" : currentRaw.trim();
        if (normalizedCurrent.isEmpty()) {
            normalizedCurrent = "-1";
        }
        String normalizedCurrent = canonicalizeCoordinateValue(currentRaw);
        boolean hasExistingCoordinate = normalizedCurrent != null && !"-1".equals(normalizedCurrent);
        JTextField inputField = new JTextField("-1".equals(normalizedCurrent) ? "" : normalizedCurrent);
        JTextField inputField = new JTextField(hasExistingCoordinate ? normalizedCurrent : "");
        inputField.setColumns(28);
        JPanel dialogPanel = new JPanel(new BorderLayout(0, 8));
        JLabel hintLabel = new JLabel("请输入新的基准站坐标(格式示例:2324.194945,N,11330.938547,E)");
        JLabel hintLabel = new JLabel("请输入新的基准站坐标(示例:3949.90238860,N,11616.75692000,E)");
        hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        dialogPanel.add(hintLabel, BorderLayout.NORTH);
        dialogPanel.add(inputField, BorderLayout.CENTER);
    JOptionPane optionPane = new JOptionPane(dialogPanel,
        JOptionPane.PLAIN_MESSAGE,
        JOptionPane.OK_CANCEL_OPTION);
    JDialog dialog = optionPane.createDialog(this, "修改基准站位置");
    dialog.setModal(true);
    dialog.pack();
        JOptionPane optionPane = new JOptionPane(dialogPanel,
            JOptionPane.PLAIN_MESSAGE,
            JOptionPane.OK_CANCEL_OPTION);
        JDialog dialog = optionPane.createDialog(this, "修改基准站位置");
        dialog.setModal(true);
        dialog.pack();
        Dimension packedSize = dialog.getSize();
        Window ownerWindow = getOwner();
@@ -208,12 +207,11 @@
            referenceWidth = 400;
        }
        dialog.setSize(new Dimension(referenceWidth, packedSize.height));
    dialog.setResizable(false);
    dialog.setLocationRelativeTo(this);
    dialog.setVisible(true);
    Object selectedValue = optionPane.getValue();
    if (!(selectedValue instanceof Integer) || ((Integer) selectedValue) != JOptionPane.OK_OPTION) {
        dialog.setResizable(false);
        dialog.setLocationRelativeTo(this);
        dialog.setVisible(true);
        Object selectedValue = optionPane.getValue();
        if (!(selectedValue instanceof Integer) || ((Integer) selectedValue) != JOptionPane.OK_OPTION) {
            return;
        }
@@ -234,7 +232,7 @@
            }
        }
        String valueToSave = trimmed.isEmpty() ? "-1" : trimmed;
        String valueToSave = trimmed.isEmpty() ? "-1" : canonicalizeCoordinateValue(trimmed);
        if (!"-1".equals(valueToSave) && !looksLikeCoordinateFormat(valueToSave)) {
            int confirm = JOptionPane.showConfirmDialog(this,
@@ -247,7 +245,7 @@
            }
        }
        if (valueToSave.equals(normalizedCurrent)) {
        if (normalizedCurrent.equals(valueToSave)) {
            JOptionPane.showMessageDialog(this,
                    "基准站位置未发生变化。",
                    "提示",
@@ -425,26 +423,11 @@
            return "设备没有安装固定";
        }
        String coordinates = baseStation.getInstallationCoordinates();
        if (coordinates == null) {
        String canonical = canonicalizeCoordinateValue(coordinates);
        if (canonical == null || "-1".equals(canonical)) {
            return "设备没有安装固定";
        }
        String trimmed = coordinates.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return "设备没有安装固定";
        }
        try {
            String[] parts = trimmed.split(",");
            if (parts.length == 4) {
                String lat = formatCoordinate(parts[0].trim(), true);
                String latDir = parts[1].trim();
                String lon = formatCoordinate(parts[2].trim(), false);
                String lonDir = parts[3].trim();
                return String.format("%s°%s, %s°%s", lat, latDir, lon, lonDir);
            }
        } catch (Exception e) {
            // ignore formatting errors and fall back to raw value
        }
        return trimmed;
        return canonical;
    }
    private String getSimCardDisplay() {
@@ -497,22 +480,93 @@
        return !trimmed.isEmpty() && !"-1".equals(trimmed);
    }
    
    private String formatCoordinate(String coordinate, boolean isLatitude) {
        // æ ¼å¼åŒ–坐标:2324.194945 -> 23°24.1949'
        try {
            // åº¦æ•°æ˜¯å‰2位(纬度)或3位(经度)
            int degreeDigits = isLatitude ? 2 : 3;
            String degreeStr = coordinate.substring(0, degreeDigits);
            String minuteStr = coordinate.substring(degreeDigits);
            // ä¿ç•™4位小数
            double minutes = Double.parseDouble(minuteStr);
            String formattedMinutes = String.format("%.4f", minutes);
            return degreeStr + "°" + formattedMinutes + "'";
        } catch (Exception e) {
            return coordinate;
    // Normalizes coordinate strings into degree-minute format when possible for consistent display/storage
    private String canonicalizeCoordinateValue(String value) {
        if (value == null) {
            return "-1";
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return "-1";
        }
        String[] parts = trimmed.split(",");
        if (parts.length != 4) {
            return trimmed;
        }
        String latDirRaw = parts[1] == null ? "" : parts[1].trim();
        String lonDirRaw = parts[3] == null ? "" : parts[3].trim();
        if (latDirRaw.isEmpty() || lonDirRaw.isEmpty()) {
            return trimmed;
        }
        String latDir = latDirRaw.toUpperCase(Locale.ROOT);
        String lonDir = lonDirRaw.toUpperCase(Locale.ROOT);
        if (!isValidDirection(latDir, true) || !isValidDirection(lonDir, false)) {
            return trimmed;
        }
        String formattedLat = convertToDegreeMinuteString(parts[0], latDir, true);
        String formattedLon = convertToDegreeMinuteString(parts[2], lonDir, false);
        if (formattedLat == null || formattedLon == null) {
            return trimmed;
        }
        return formattedLat + "," + latDir + "," + formattedLon + "," + lonDir;
    }
    private boolean isValidDirection(String direction, boolean isLatitude) {
        if (direction == null) {
            return false;
        }
        if (isLatitude) {
            return "N".equals(direction) || "S".equals(direction);
        }
        return "E".equals(direction) || "W".equals(direction);
    }
    // Converts decimal degrees or degree-minute input into a canonical degree-minute string (8 decimal places)
    private String convertToDegreeMinuteString(String rawValue, String direction, boolean isLatitude) {
        double decimal = parseCoordinateToDecimalDegrees(rawValue, direction, isLatitude);
        if (!Double.isFinite(decimal)) {
            return null;
        }
        double absDecimal = Math.abs(decimal);
        int degrees = (int) Math.floor(absDecimal);
        double minutes = (absDecimal - degrees) * 60.0d;
        double degreeMinutes = degrees * 100.0d + minutes;
        return String.format(Locale.US, "%.8f", degreeMinutes);
    }
    private double parseCoordinateToDecimalDegrees(String rawValue, String direction, boolean isLatitude) {
        if (rawValue == null) {
            return Double.NaN;
        }
        String trimmed = rawValue.trim();
        if (trimmed.isEmpty()) {
            return Double.NaN;
        }
        double numeric;
        try {
            numeric = Double.parseDouble(trimmed);
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
        double abs = Math.abs(numeric);
        double boundary = isLatitude ? 90d : 180d;
        double decimal;
        if (abs <= boundary) {
            decimal = abs;
        } else {
            double degrees = Math.floor(abs / 100d);
            double minutes = abs - degrees * 100d;
            decimal = degrees + minutes / 60d;
        }
        String dirUpper = direction == null ? "" : direction.trim().toUpperCase(Locale.ROOT);
        if ("S".equals(dirUpper) || "W".equals(dirUpper)) {
            decimal = -decimal;
        } else if (!"N".equals(dirUpper) && !"E".equals(dirUpper) && numeric < 0d) {
            decimal = -decimal;
        }
        return decimal;
    }
    
    private void lockBaseStationPosition() {
@@ -542,7 +596,7 @@
                JOptionPane.INFORMATION_MESSAGE);
            
            // æ›´æ–°åŸºå‡†ç«™ä½ç½®ï¼ˆè¿™é‡Œä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®ï¼Œå®žé™…应从GPS获取)
            String newPosition = "2324.194945,N,11330.938547,E";
            String newPosition = canonicalizeCoordinateValue("2324.194945,N,11330.938547,E");
            String timestamp = String.valueOf(System.currentTimeMillis());
            if (!hasBaseStationId()) {
src/bianjie/BoundaryAlgorithm.java
@@ -488,19 +488,22 @@
        if (!optimized.get(0).equals(optimized.get(optimized.size() - 1))) {
            Coordinate first = optimized.get(0);
            Coordinate last = optimized.get(optimized.size() - 1);
            double closingDistance = calculateDistance(first, last);
            double closingDistance = calculateDistance(last, first);
            if (closingDistance > params.interval) {
                // å¦‚果首尾距离较远,添加中间点
                List<Coordinate> interpolated = interpolateBoundary(
                    List.of(last, first), params.interval
                );
                optimized.addAll(interpolated.subList(1, interpolated.size() - 1));
                int segments = (int) Math.ceil(closingDistance / params.interval);
                for (int i = 1; i < segments; i++) {
                    double ratio = (double) i / segments;
                    Coordinate interpolatedPoint = interpolate(last, first, ratio);
                    if (!interpolatedPoint.equals(last)) {
                        optimized.add(interpolatedPoint);
                    }
                }
            }
            optimized.add(first);
        }
        return optimized;
        return removeConsecutiveDuplicates(optimized);
    }
    // é“格拉斯-普克算法
@@ -580,6 +583,26 @@
        return interpolated;
    }
    private List<Coordinate> removeConsecutiveDuplicates(List<Coordinate> points) {
        if (points == null || points.isEmpty()) {
            return points;
        }
        List<Coordinate> cleaned = new ArrayList<>();
        Coordinate previous = null;
        for (Coordinate point : points) {
            if (previous == null || !point.equals(previous)) {
                cleaned.add(point);
                previous = point;
            }
        }
        if (!cleaned.isEmpty() && !cleaned.get(0).equals(cleaned.get(cleaned.size() - 1))) {
            cleaned.add(cleaned.get(0));
        }
        return cleaned;
    }
    // çº¿æ€§æ’值
    private Coordinate interpolate(Coordinate p1, Coordinate p2, double ratio) {
        double x = p1.x + (p2.x - p1.x) * ratio;
src/bianjie/bianjieguihua.java
ÎļþÒÑɾ³ý
src/bianjie/bianjieguihua2.java
@@ -34,9 +34,30 @@
            double baseLat = parseDMToDecimal(baseParts[0], baseParts[1]);
            double baseLon = parseDMToDecimal(baseParts[2], baseParts[3]);
            
            // å°†Coordinate列表转换为局部坐标系坐标
            List<BoundaryAlgorithm.Coordinate> localCoordinates =
                convertToLocalCoordinates(coordinates, baseLat, baseLon);
            // å°†Coordinate列表转换为局部坐标系坐标
            List<BoundaryAlgorithm.Coordinate> localCoordinates =
                convertToLocalCoordinates(coordinates, baseLat, baseLon);
            // ä¸‰è§’形小区域特殊处理,避免过度插值导致点数扩增
            if (localCoordinates.size() == 3) {
                double triangleArea = calculatePolygonArea(localCoordinates);
                double trianglePerimeter = calculatePerimeter(localCoordinates);
                System.out.println("检测到三角形边界,面积=" + String.format("%.2f", triangleArea) +
                        "m², å‘¨é•¿=" + String.format("%.2f", trianglePerimeter) + "m");
                if (triangleArea < 100.0 || trianglePerimeter < 30.0) {
                    System.out.println("小三角形,跳过插值优化");
                    BoundaryAlgorithm.Coordinate firstPoint = localCoordinates.get(0);
                    List<BoundaryAlgorithm.Coordinate> trianglePoints = new ArrayList<>(localCoordinates);
                    trianglePoints.add(new BoundaryAlgorithm.Coordinate(
                            firstPoint.x,
                            firstPoint.y,
                            firstPoint.lat,
                            firstPoint.lon));
                    return convertBoundaryPointsToString(trianglePoints);
                }
            }
            
            // åˆ›å»ºç®—法实例
            BoundaryAlgorithm algorithm = new BoundaryAlgorithm();
@@ -264,6 +285,16 @@
        
        return Math.abs(area) / 2.0;
    }
    private static double calculatePerimeter(List<BoundaryAlgorithm.Coordinate> points) {
        if (points == null || points.size() != 3) {
            return 0.0;
        }
        double d1 = calculateDistance(points.get(0), points.get(1));
        double d2 = calculateDistance(points.get(1), points.get(2));
        double d3 = calculateDistance(points.get(2), points.get(0));
        return d1 + d2 + d3;
    }
    
    // ============ å…¶ä»–方法保持不变 ============
    
src/chuankou/SerialPortNativeLoader.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
package chuankou;
import java.io.File;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
 * Ensures the correct jSerialComm native library is available on Windows/x86_64 systems.
 */
public final class SerialPortNativeLoader {
    private static final String LIB_PROPERTY = "com.fazecast.jSerialComm.library.path";
    private static final String EXPECTED_DLL = "jSerialComm.dll";
    private SerialPortNativeLoader() {
        // utility class
    }
    public static void ensureLibraryPresent() {
        if (System.getProperty(LIB_PROPERTY) != null) {
            return;
        }
        String osName = System.getProperty("os.name", "").toLowerCase();
        if (!osName.contains("win")) {
            return;
        }
        String arch = System.getProperty("os.arch", "").toLowerCase();
        if (!arch.contains("64")) {
            return;
        }
        Path candidateDir = Paths.get("lib", "native", "windows", "x86_64").toAbsolutePath();
        File dllFile = candidateDir.resolve(EXPECTED_DLL).toFile();
        if (!dllFile.isFile()) {
            candidateDir = resolveFromCodeSource();
            if (candidateDir != null) {
                dllFile = candidateDir.resolve(EXPECTED_DLL).toFile();
            }
        }
        if (dllFile.isFile()) {
            System.setProperty(LIB_PROPERTY, candidateDir.toString());
        } else {
            System.err.println("Expected jSerialComm native library not found. Checked " + dllFile);
        }
    }
    private static Path resolveFromCodeSource() {
        try {
            java.security.CodeSource codeSource = SerialPortNativeLoader.class.getProtectionDomain().getCodeSource();
            if (codeSource == null) {
                return null;
            }
            Path location = Paths.get(codeSource.getLocation().toURI()).toAbsolutePath();
            Path baseDir = location.getParent();
            if (baseDir == null) {
                return null;
            }
            Path siblingLibDir = baseDir.resolveSibling("lib").resolve("native").resolve("windows").resolve("x86_64");
            if (siblingLibDir.toFile().isDirectory()) {
                return siblingLibDir;
            }
            Path relativeLibDir = baseDir.resolve("lib").resolve("native").resolve("windows").resolve("x86_64");
            if (relativeLibDir.toFile().isDirectory()) {
                return relativeLibDir;
            }
        } catch (URISyntaxException | SecurityException ignored) {
            // ignore
        }
        return null;
    }
}
src/chuankou/SerialPortService.java
@@ -1,13 +1,17 @@
package chuankou;
import com.fazecast.jSerialComm.SerialPort;
import java.util.function.Consumer;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.Consumer;
public class SerialPortService {
    static {
        SerialPortNativeLoader.ensureLibraryPresent();
    }
    private SerialPort port;
    private volatile boolean capturing = false;
    private volatile boolean paused = true;
src/denglu/Denglu.java
@@ -1,6 +1,7 @@
package denglu;
import homein.WenJianSuo;
import ui.UIConfig;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
@@ -35,7 +36,7 @@
    private void initializeTranslations() {
        translations = new HashMap<>();
        // ä¸­æ–‡ç¿»è¯‘
        Map<String, String> zh = new HashMap<>();
        zh.put("username_label", "用户名或邮箱");
@@ -45,7 +46,9 @@
        zh.put("login_button", "登录");
        zh.put("no_account", "还没有账户?");
        zh.put("sign_up", "立即注册");
        zh.put("app_info", "AutoMow Â© 2023 - æ™ºèƒ½å‰²è‰è§£å†³æ–¹æ¡ˆ");
        zh.put("app_title", "智能割草系统");
        zh.put("window_title", "智能割草系统 - ç™»å½•");
        zh.put("app_info", "智能割草系统 Â© 2023 - æ™ºèƒ½å‰²è‰è§£å†³æ–¹æ¡ˆ");
        zh.put("register_button", "注册新账户");
        translations.put("zh", zh);
@@ -58,7 +61,9 @@
        en.put("login_button", "Sign In");
        en.put("no_account", "Don't have an account?");
        en.put("sign_up", "Sign Up");
        en.put("app_info", "AutoMow Â© 2023 - Smart Mowing Solutions");
        en.put("app_title", "Smart Mowing System");
        en.put("window_title", "Smart Mowing System - Login");
        en.put("app_info", "Smart Mowing System Â© 2023 - Intelligent Lawn Care");
        en.put("register_button", "Create New Account");
        translations.put("en", en);
@@ -71,7 +76,9 @@
        es.put("login_button", "Iniciar Sesión");
        es.put("no_account", "¿No tienes una cuenta?");
        es.put("sign_up", "Regístrate");
        es.put("app_info", "AutoMow Â© 2023 - Soluciones de Corte Inteligente");
        es.put("app_title", "Sistema de Corte Inteligente");
        es.put("window_title", "Sistema de Corte Inteligente - Iniciar Sesión");
        es.put("app_info", "Sistema de Corte Inteligente Â© 2023 - Soluciones Inteligentes");
        es.put("register_button", "Crear Nueva Cuenta");
        translations.put("es", es);
@@ -84,8 +91,10 @@
        fr.put("login_button", "Se connecter");
        fr.put("no_account", "Vous n'avez pas de compte?");
        fr.put("sign_up", "S'inscrire");
        fr.put("app_info", "AutoMow Â© 2023 - Solutions de Tonte Intelligente");
        es.put("register_button", "Créer un Nouveau Compte");
        fr.put("app_title", "Système de Tonte Intelligent");
        fr.put("window_title", "Système de Tonte Intelligent - Connexion");
        fr.put("app_info", "Système de Tonte Intelligent Â© 2023 - Solutions de Tonte");
        fr.put("register_button", "Créer un Nouveau Compte");
        translations.put("fr", fr);
        // å¾·è¯­ç¿»è¯‘
@@ -97,18 +106,20 @@
        de.put("login_button", "Anmelden");
        de.put("no_account", "Sie haben noch kein Konto?");
        de.put("sign_up", "Registrieren");
        de.put("app_info", "AutoMow Â© 2023 - Intelligente Mäh-Lösungen");
        de.put("app_title", "Intelligentes Mähsystem");
        de.put("window_title", "Intelligentes Mähsystem - Anmeldung");
        de.put("app_info", "Intelligentes Mähsystem Â© 2023 - Mählösungen");
        de.put("register_button", "Neues Konto Erstellen");
        translations.put("de", de);
    }
    private void initializeUI() {
        setTitle("AutoMow - ç™»å½•");
        setTitle(getTranslationValue(currentLanguageCode, "window_title", "系统登录"));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // è®¾ç½®ç´§å‡‘布局尺寸
        setSize(320, 520);
        setMinimumSize(new Dimension(300, 500));
        setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT);
        setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT));
        setResizable(false);
        
        setupCompactLayout();
@@ -159,11 +170,6 @@
        ));
        
        // ç™»å½•标题
        JLabel loginTitle = new JLabel("AutoMow", SwingConstants.CENTER);
        loginTitle.setFont(new Font("PingFang SC", Font.BOLD, 24));
        loginTitle.setForeground(THEME_COLOR);
        loginTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
        // ç”¨æˆ·åè¾“å…¥
        usernameLabel = new JLabel("用户名或邮箱", SwingConstants.CENTER);
        usernameLabel.setFont(new Font("PingFang SC", Font.BOLD, 13));
@@ -212,7 +218,7 @@
        rememberForgotPanel.add(rememberMe, BorderLayout.WEST);
        rememberForgotPanel.add(forgotPassword, BorderLayout.EAST);
        
        // ç™»å½•按钮 - æ°´å¹³å±…中,长度与文本框相同
        // ç™»å½•按钮 - æ°´å¹³å±…中,长度与文本框宽度保持相近
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        buttonPanel.setBackground(Color.WHITE);
        buttonPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50));
@@ -221,8 +227,8 @@
        loginButton.setBackground(THEME_COLOR);
        loginButton.setForeground(Color.WHITE);
        loginButton.setFont(new Font("PingFang SC", Font.BOLD, 15));
        // è®¾ç½®ç™»å½•按钮长度与文本框相同
        loginButton.setPreferredSize(new Dimension(260, 42));
        // è®¾ç½®ç™»å½•按钮长度接近文本框宽度
        loginButton.setPreferredSize(new Dimension(300, 48));
        loginButton.setBorderPainted(false);
        loginButton.setFocusPainted(false);
        loginButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
@@ -248,7 +254,7 @@
        registerButton.setBackground(Color.WHITE);
        registerButton.setForeground(THEME_COLOR);
        registerButton.setFont(new Font("PingFang SC", Font.BOLD, 14));
        registerButton.setPreferredSize(new Dimension(140, 38));
        registerButton.setPreferredSize(new Dimension(180, 42));
        registerButton.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(THEME_COLOR),
            BorderFactory.createEmptyBorder(5, 15, 5, 15)
@@ -282,12 +288,11 @@
        signupPanel.add(signupLink);
        
        // ç»„装表单
        formPanel.add(loginTitle);
        formPanel.add(Box.createRigidArea(new Dimension(0, 25)));
    formPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        formPanel.add(usernameLabel);
        formPanel.add(Box.createRigidArea(new Dimension(0, 5)));
        formPanel.add(usernameField);
        formPanel.add(Box.createRigidArea(new Dimension(0, 15)));
    formPanel.add(Box.createRigidArea(new Dimension(0, 30)));
        formPanel.add(passwordLabel);
        formPanel.add(Box.createRigidArea(new Dimension(0, 5)));
        formPanel.add(passwordField);
@@ -307,7 +312,9 @@
        JPanel appInfoPanel = new JPanel();
        appInfoPanel.setBackground(Color.WHITE);
        appInfoPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 5, 0));
        appInfoLabel = new JLabel("AutoMow Â© 2023 - æ™ºèƒ½å‰²è‰è§£å†³æ–¹æ¡ˆ");
        appInfoLabel = new JLabel(
            getTranslationValue(currentLanguageCode, "app_info", "智能割草系统 Â© 2023")
        );
        appInfoLabel.setFont(new Font("PingFang SC", Font.PLAIN, 10));
        appInfoLabel.setForeground(new Color(150, 150, 150));
        appInfoPanel.add(appInfoLabel);
@@ -356,20 +363,20 @@
        
        if (translation != null) {
            // æ›´æ–°æ‰€æœ‰ç•Œé¢æ–‡æœ¬
            usernameLabel.setText(translation.get("username_label"));
            passwordLabel.setText(translation.get("password_label"));
            rememberMe.setText(translation.get("remember_me"));
            forgotPassword.setText(translation.get("forgot_password"));
            loginButton.setText(translation.get("login_button"));
            noAccountLabel.setText(translation.get("no_account"));
            signupLink.setText(translation.get("sign_up"));
            appInfoLabel.setText(translation.get("app_info"));
            usernameLabel.setText(getTranslationValue(langCode, "username_label", usernameLabel.getText()));
            passwordLabel.setText(getTranslationValue(langCode, "password_label", passwordLabel.getText()));
            rememberMe.setText(getTranslationValue(langCode, "remember_me", rememberMe.getText()));
            forgotPassword.setText(getTranslationValue(langCode, "forgot_password", forgotPassword.getText()));
            loginButton.setText(getTranslationValue(langCode, "login_button", loginButton.getText()));
            noAccountLabel.setText(getTranslationValue(langCode, "no_account", noAccountLabel.getText()));
            signupLink.setText(getTranslationValue(langCode, "sign_up", signupLink.getText()));
            appInfoLabel.setText(getTranslationValue(langCode, "app_info", appInfoLabel.getText()));
            // æ›´æ–°æ³¨å†ŒæŒ‰é’®æ–‡æœ¬
            registerButton.setText(translation.get("register_button"));
            registerButton.setText(getTranslationValue(langCode, "register_button", registerButton.getText()));
            // æ›´æ–°çª—口标题
            setTitle("AutoMow - " + (langCode.equals("zh") ? "登录" : "Login"));
            setTitle(getTranslationValue(langCode, "window_title", getTitle()));
            
            // æ›´æ–°è¯­è¨€è®¾ç½®åˆ°æ–‡ä»¶
            updateLanguagePreference(langCode);
@@ -387,6 +394,17 @@
        }
    }
    private String getTranslationValue(String languageCode, String key, String defaultValue) {
        Map<String, String> translation = translations.get(languageCode);
        if (translation != null) {
            String value = translation.get(key);
            if (value != null && !value.trim().isEmpty()) {
                return value;
            }
        }
        return defaultValue;
    }
    private void updateLanguagePreference(String languageCode) {
        String currentLanguage = UserChuShiHua.getProperty("language");
        if (!languageCode.equals(currentLanguage)) {
@@ -512,10 +530,5 @@
        RegistrationFrame registerFrame = new RegistrationFrame(this, currentLanguageCode);
        registerFrame.setVisible(true);
    }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new Denglu().setVisible(true);
        });
    }
}
src/denglu/RegistrationFrame.java
@@ -1,5 +1,6 @@
package denglu;
import ui.UIConfig;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
@@ -44,7 +45,8 @@
        
        // ä¸­æ–‡ç¿»è¯‘
        Map<String, String> zh = new HashMap<>();
        zh.put("title", "用户注册");
    zh.put("title", "用户注册");
    zh.put("window_title", "智能割草系统 - ç”¨æˆ·æ³¨å†Œ");
        zh.put("username_label", "用户名");
        zh.put("email_label", "邮箱地址");
        zh.put("verification_code_label", "验证码");
@@ -66,7 +68,8 @@
        // è‹±æ–‡ç¿»è¯‘
        Map<String, String> en = new HashMap<>();
        en.put("title", "User Registration");
    en.put("title", "User Registration");
    en.put("window_title", "Smart Mowing System - User Registration");
        en.put("username_label", "Username");
        en.put("email_label", "Email Address");
        en.put("verification_code_label", "Verification Code");
@@ -88,7 +91,8 @@
        // è¥¿ç­ç‰™è¯­ç¿»è¯‘
        Map<String, String> es = new HashMap<>();
        es.put("title", "Registro de Usuario");
    es.put("title", "Registro de Usuario");
    es.put("window_title", "Sistema de Corte Inteligente - Registro de Usuario");
        es.put("username_label", "Usuario");
        es.put("email_label", "Correo Electrónico");
        es.put("verification_code_label", "Código de Verificación");
@@ -110,7 +114,8 @@
        // æ³•语翻译
        Map<String, String> fr = new HashMap<>();
        fr.put("title", "Inscription Utilisateur");
    fr.put("title", "Inscription Utilisateur");
    fr.put("window_title", "Système de Tonte Intelligent - Inscription");
        fr.put("username_label", "Nom d'utilisateur");
        fr.put("email_label", "Adresse Email");
        fr.put("verification_code_label", "Code de Vérification");
@@ -132,7 +137,8 @@
        // å¾·è¯­ç¿»è¯‘
        Map<String, String> de = new HashMap<>();
        de.put("title", "Benutzerregistrierung");
    de.put("title", "Benutzerregistrierung");
    de.put("window_title", "Intelligentes Mähsystem - Registrierung");
        de.put("username_label", "Benutzername");
        de.put("email_label", "E-Mail-Adresse");
        de.put("verification_code_label", "Verifizierungscode");
@@ -153,10 +159,22 @@
        translations.put("de", de);
    }
    private String getTranslationValue(String languageCode, String key, String defaultValue) {
        Map<String, String> translation = translations.get(languageCode);
        if (translation != null) {
            String value = translation.get(key);
            if (value != null && !value.trim().isEmpty()) {
                return value;
            }
        }
        return defaultValue;
    }
    private void initializeUI() {
        setTitle("AutoMow - ç”¨æˆ·æ³¨å†Œ");
    setTitle(getTranslationValue(currentLanguageCode, "window_title", "用户注册"));
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        setSize(350, 500);
    setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT);
    setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT));
        setLocationRelativeTo(parentFrame);
        setResizable(false);
        
@@ -372,15 +390,15 @@
        
        if (translation != null) {
            // æ›´æ–°æ‰€æœ‰ç•Œé¢æ–‡æœ¬
            setTitle("AutoMow - " + translation.get("title"));
            userLabel.setText(translation.get("username_label"));
            emailLabel.setText(translation.get("email_label"));
            verificationCodeLabel.setText(translation.get("verification_code_label"));
            passLabel.setText(translation.get("password_label"));
            confirmPassLabel.setText(translation.get("confirm_password_label"));
            registerButton.setText(translation.get("register_button"));
            cancelButton.setText(translation.get("cancel_button"));
            sendCodeButton.setText(translation.get("send_code_button"));
            setTitle(getTranslationValue(languageCode, "window_title", getTitle()));
            userLabel.setText(getTranslationValue(languageCode, "username_label", userLabel.getText()));
            emailLabel.setText(getTranslationValue(languageCode, "email_label", emailLabel.getText()));
            verificationCodeLabel.setText(getTranslationValue(languageCode, "verification_code_label", verificationCodeLabel.getText()));
            passLabel.setText(getTranslationValue(languageCode, "password_label", passLabel.getText()));
            confirmPassLabel.setText(getTranslationValue(languageCode, "confirm_password_label", confirmPassLabel.getText()));
            registerButton.setText(getTranslationValue(languageCode, "register_button", registerButton.getText()));
            cancelButton.setText(getTranslationValue(languageCode, "cancel_button", cancelButton.getText()));
            sendCodeButton.setText(getTranslationValue(languageCode, "send_code_button", sendCodeButton.getText()));
        }
    }
src/dikuai/Dikuaiguanli.java
@@ -6,6 +6,9 @@
import java.awt.datatransfer.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import ui.UIConfig;
@@ -17,6 +20,8 @@
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import lujing.Lunjingguihua;
import zhangaiwu.AddDikuai;
@@ -58,6 +63,8 @@
    private JButton addLandBtn;
    private static String currentWorkLandNumber;
    private static final String WORK_LAND_KEY = "currentWorkLandNumber";
    private static final String PROPERTIES_FILE = "set.properties";
    private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>();
    private ImageIcon workSelectedIcon;
    private ImageIcon workUnselectedIcon;
@@ -504,6 +511,7 @@
                    boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber);
                    if (isCurrent) {
                        renderer.setBoundaryPointsVisible(desiredState);
                        renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d);
                    }
                }
            }
@@ -583,17 +591,69 @@
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setPreferredSize(new Dimension(360, 240));
        int option = JOptionPane.showConfirmDialog(
            this,
            scrollPane,
            title,
            JOptionPane.OK_CANCEL_OPTION,
            JOptionPane.PLAIN_MESSAGE);
        if (option == JOptionPane.OK_OPTION) {
            return textArea.getText();
        Window owner = SwingUtilities.getWindowAncestor(this);
        JDialog dialog;
        if (owner instanceof Frame) {
            dialog = new JDialog((Frame) owner, title, true);
        } else if (owner instanceof Dialog) {
            dialog = new JDialog((Dialog) owner, title, true);
        } else {
            dialog = new JDialog((Frame) null, title, true);
        }
        return null;
        dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        JPanel contentPanel = new JPanel(new BorderLayout());
        contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        contentPanel.add(scrollPane, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        JButton okButton = new JButton("确定");
        JButton cancelButton = new JButton("取消");
        JButton copyButton = new JButton("复制");
        final boolean[] confirmed = new boolean[] {false};
        final String[] resultHolder = new String[1];
        okButton.addActionListener(e -> {
            resultHolder[0] = textArea.getText();
            confirmed[0] = true;
            dialog.dispose();
        });
        cancelButton.addActionListener(e -> dialog.dispose());
        copyButton.addActionListener(e -> {
            String text = textArea.getText();
            if (text == null) {
                text = "";
            }
            String trimmed = text.trim();
            if (trimmed.isEmpty() || "-1".equals(trimmed)) {
                JOptionPane.showMessageDialog(dialog, title + " æœªè®¾ç½®", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            try {
                StringSelection selection = new StringSelection(text);
                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                clipboard.setContents(selection, selection);
                JOptionPane.showMessageDialog(dialog, title + " å·²å¤åˆ¶åˆ°å‰ªè´´æ¿", "提示", JOptionPane.INFORMATION_MESSAGE);
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(dialog, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            }
        });
        buttonPanel.add(okButton);
        buttonPanel.add(cancelButton);
        buttonPanel.add(copyButton);
        contentPanel.add(buttonPanel, BorderLayout.SOUTH);
        dialog.setContentPane(contentPanel);
        dialog.getRootPane().setDefaultButton(okButton);
        dialog.pack();
        dialog.setLocationRelativeTo(this);
        dialog.setVisible(true);
        return confirmed[0] ? resultHolder[0] : null;
    }
    private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) {
@@ -733,10 +793,26 @@
        switch (trimmed) {
            case "1":
            case "spiral":
            case "螺旋":
            case "螺旋模式":
                return "spiral";
            case "0":
            case "parallel":
            case "平行":
            case "平行模式":
            default:
                if (trimmed.contains("螺旋")) {
                    return "spiral";
                }
                if (trimmed.contains("spiral")) {
                    return "spiral";
                }
                if (trimmed.contains("parallel")) {
                    return "parallel";
                }
                if (trimmed.contains("平行")) {
                    return "parallel";
                }
                return "parallel";
        }
    }
@@ -1446,10 +1522,19 @@
    }
    public static void setCurrentWorkLand(String landNumber, String landName) {
        currentWorkLandNumber = landNumber;
        String sanitizedLandNumber = sanitizeLandNumber(landNumber);
        boolean changed = !Objects.equals(currentWorkLandNumber, sanitizedLandNumber);
        if (!changed) {
            String persisted = readPersistedWorkLandNumber();
            if (!Objects.equals(persisted, sanitizedLandNumber)) {
                changed = true;
            }
        }
        currentWorkLandNumber = sanitizedLandNumber;
        Dikuai dikuai = null;
        if (landNumber != null) {
            dikuai = Dikuai.getDikuai(landNumber);
        if (sanitizedLandNumber != null) {
            dikuai = Dikuai.getDikuai(sanitizedLandNumber);
            if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) {
                landName = dikuai.getLandName();
            }
@@ -1460,7 +1545,7 @@
        Shouye shouye = Shouye.getInstance();
        if (shouye != null) {
            if (landNumber == null) {
            if (sanitizedLandNumber == null) {
                shouye.updateCurrentAreaName(null);
            } else {
                shouye.updateCurrentAreaName(landName);
@@ -1470,24 +1555,85 @@
                renderer.applyLandMetadata(dikuai);
                String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
                String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
                List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : Collections.emptyList();
                List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList();
                if (configuredObstacles == null) {
                    configuredObstacles = Collections.emptyList();
                }
                renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
                renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName);
                renderer.setCurrentPlannedPath(plannedPath);
                renderer.setCurrentObstacles(configuredObstacles, landNumber);
                boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false);
                renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber);
                boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false);
                renderer.setBoundaryPointsVisible(showBoundaryPoints);
                renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d);
            }
            shouye.refreshMowingIndicators();
        }
        if (changed) {
            persistCurrentWorkLand(sanitizedLandNumber);
        }
    }
    public static String getCurrentWorkLandNumber() {
        return currentWorkLandNumber;
    }
    public static String getPersistedWorkLandNumber() {
        return readPersistedWorkLandNumber();
    }
    private static String sanitizeLandNumber(String landNumber) {
        if (landNumber == null) {
            return null;
        }
        String trimmed = landNumber.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
        }
        return trimmed;
    }
    private static void persistCurrentWorkLand(String landNumber) {
        synchronized (Dikuaiguanli.class) {
            Properties props = new Properties();
            try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) {
                props.load(in);
            } catch (IOException ignored) {
                // Use empty defaults when the configuration file is missing.
            }
            if (landNumber == null) {
                props.setProperty(WORK_LAND_KEY, "-1");
            } else {
                props.setProperty(WORK_LAND_KEY, landNumber);
            }
            try (FileOutputStream out = new FileOutputStream(PROPERTIES_FILE)) {
                props.store(out, "Current work land selection updated");
            } catch (IOException ex) {
                System.err.println("无法保存当前作业地块: " + ex.getMessage());
            }
        }
    }
    private static String readPersistedWorkLandNumber() {
        Properties props = new Properties();
        try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) {
            props.load(in);
            String value = props.getProperty(WORK_LAND_KEY);
            if (value == null) {
                return null;
            }
            String trimmed = value.trim();
            if (trimmed.isEmpty() || "-1".equals(trimmed)) {
                return null;
            }
            return trimmed;
        } catch (IOException ex) {
            return null;
        }
    }
    private ImageIcon loadIcon(String path, int width, int height) {
        try {
            ImageIcon rawIcon = new ImageIcon(path);
src/dikuai/addzhangaiwu.java
@@ -46,6 +46,7 @@
import set.Setsys;
import ui.UIConfig;
import zhuye.Coordinate;
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
@@ -887,6 +888,15 @@
        activeDrawingShape = shape.toLowerCase(Locale.ROOT);
        Shouye shouyeInstance = Shouye.getInstance();
        if (shouyeInstance != null) {
            shouyeInstance.setHandheldMowerIconActive("handheld".equalsIgnoreCase(method));
            MapRenderer renderer = shouyeInstance.getMapRenderer();
            if (renderer != null) {
                renderer.clearIdleTrail();
            }
        }
        Coordinate.coordinates.clear();
        Coordinate.setActiveDeviceIdFilter(deviceId);
        Coordinate.setStartSaveGngga(true);
src/gecaoji/Gecaoji.java
@@ -15,27 +15,35 @@
    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/gecaoji.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;
        }
    }
@@ -51,7 +59,7 @@
        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;
        }
@@ -109,16 +117,17 @@
        }
    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);
        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);
@@ -132,7 +141,7 @@
        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);
    }
@@ -177,4 +186,19 @@
        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;
    }
}
src/gecaoji/lujingdraw.java
@@ -70,7 +70,7 @@
    /** // æ–‡æ¡£æ³¨é‡Šå¼€å§‹
     * Draw the planned mowing path. // ç»˜åˆ¶è·¯å¾„
     */ // æ–‡æ¡£æ³¨é‡Šç»“束
    public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale) { // ç»˜åˆ¶ä¸»æ–¹æ³•
    public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale) { // ç»˜åˆ¶ä¸»æ–¹æ³•
        if (path == null || path.size() < 2) { // åˆ¤å®šç‚¹æ•°
            return; // æ•°æ®ä¸è¶³ç›´æŽ¥è¿”回
        } // if结束
@@ -97,13 +97,13 @@
        Point2D.Double end = path.get(path.size() - 1); // ç»ˆç‚¹
        Point2D.Double prev = path.get(path.size() - 2); // å€’数第二个点
        drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale); // ç»˜åˆ¶èµ·ç‚¹ç®­å¤´
        drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale); // ç»˜åˆ¶ç»ˆç‚¹ç®­å¤´
    drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // ç»˜åˆ¶èµ·ç‚¹ç®­å¤´
    drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // ç»˜åˆ¶ç»ˆç‚¹ç®­å¤´
        g2d.setStroke(previous); // æ¢å¤åŽŸæè¾¹
    } // æ–¹æ³•结束
    private static void drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale) { // ç»˜åˆ¶ç®­å¤´è¾…助
    private static void drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale, double sizeScale) { // ç»˜åˆ¶ç®­å¤´è¾…助
        if (from == null || to == null) { // åˆ¤ç©º
            return; // æ•°æ®ä¸è¶³è¿”回
        } // if结束
@@ -115,7 +115,8 @@
        } // if结束
    double arrowLength = Math.max(2.5, 5.5 / Math.max(0.5, scale)); // è®¡ç®—箭头长度
    arrowLength *= 0.5; // ç¼©å°ç®­å¤´å°ºå¯¸ä¸ºåŽŸæ¥çš„ä¸€åŠ
    double clampedScale = sizeScale > 0 ? sizeScale : 1.0; // é˜²æ­¢éžæ³•缩放
    arrowLength *= 0.25 * clampedScale; // ç¼©å°ç®­å¤´è‡³åŽŸæ¥çš„ä¸€åŠ
        double arrowWidth = arrowLength * 0.45; // è®¡ç®—箭头宽度
        double ux = dx / length; // å•位向量X
src/homein/Homein.java
@@ -3,9 +3,11 @@
import denglu.UserChuShiHua;
import gecaoji.Device;
import chuankou.SerialPortAutoConnector;
import chuankou.SerialPortNativeLoader;
import set.Setsys;
import udpdell.UDPServer;
import denglu.Denglu;
import java.awt.EventQueue;
import javax.swing.JOptionPane;
public class Homein {
@@ -26,15 +28,16 @@
        }));
        try {
                // åˆå§‹åŒ–数据
                UserChuShiHua.initialize();
            SerialPortNativeLoader.ensureLibraryPresent();
            // åˆå§‹åŒ–数据
            UserChuShiHua.initialize();
                Setsys setsys = new Setsys();
                setsys.initializeFromProperties();
                Device.initializeActiveDevice(setsys.getMowerId());
            Setsys setsys = new Setsys();
            setsys.initializeFromProperties();
            Device.initializeActiveDevice(setsys.getMowerId());
            UDPServer.startAsync();//启动数据接收线程
            SerialPortAutoConnector.initialize();//启动串口自动连接
//            SerialPortAutoConnector.initialize();//启动串口自动连接
            
            // æ˜¾ç¤ºåˆå§‹æ•°æ®
            System.out.println("初始用户名: " + UserChuShiHua.getProperty("userName"));
@@ -54,17 +57,15 @@
    
    private static void startLoginInterface() {
        // åœ¨EDT中启动登录界面
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    new Denglu().setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                    JOptionPane.showMessageDialog(null,
                        "登录界面启动失败: " + e.getMessage(),
                        "错误",
                        JOptionPane.ERROR_MESSAGE);
                }
        EventQueue.invokeLater(() -> {
            try {
                new Denglu().setVisible(true);
            } catch (Exception e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(null,
                    "登录界面启动失败: " + e.getMessage(),
                    "错误",
                    JOptionPane.ERROR_MESSAGE);
            }
        });
    }
src/lujing/Lunjingguihua.java
@@ -266,8 +266,8 @@
        }
        List<PathSegment> generate() {
            if (!"spiral".equals(mode)) {
                return generateParallelPath();
            if ("spiral".equals(mode)) {
                return generateSpiralPath();
            }
            return generateParallelPath();
        }
@@ -361,6 +361,39 @@
            return path;
        }
        private List<PathSegment> generateSpiralPath() {
            Geometry safeArea = buildSafeArea();
            if (safeArea == null || safeArea.isEmpty()) {
                System.err.println("安全区域为空,无法生成螺旋路径");
                return new ArrayList<>();
            }
            List<PathSegment> spiral = luoxuan.generateSpiralPath(safeArea, width);
            if (spiral.isEmpty()) {
                return spiral;
            }
            postProcess(spiral);
            PathSegment firstMowing = null;
            PathSegment endCandidate = null;
            for (int i = 0; i < spiral.size(); i++) {
                PathSegment seg = spiral.get(i);
                if (seg != null && seg.isMowing) {
                    if (firstMowing == null) {
                        firstMowing = seg;
                    }
                    endCandidate = seg;
                }
            }
            if (firstMowing != null) {
                firstMowing.setAsStartPoint();
            }
            if (endCandidate != null && endCandidate != firstMowing) {
                endCandidate.setAsEndPoint();
            }
            return spiral;
        }
        private Geometry buildSafeArea() {
            try {
                Coordinate[] coords = polygon.toArray(new Coordinate[0]);
src/lujing/luoxuan.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,408 @@
package lujing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.operation.buffer.BufferParameters;
import lujing.Lunjingguihua.PathSegment;
/**
 * Utility class that produces spiral mowing paths by iteratively offsetting a safe area polygon.
 * ä¼˜åŒ–版:改进路径生成逻辑,减少空驶距离,优化路径连续性
 */
public final class luoxuan {
    private static final int MAX_ITERATIONS = 512;
    private static final double AREA_EPSILON = 1e-2;
    private static final double LENGTH_EPSILON = 1e-6;
    private static final double MIN_BUFFER_RATIO = 0.6;  // æœ€å°ç¼“冲比例
    private luoxuan() {
    }
    /**
     * Generate optimized spiral mowing paths with improved continuity
     */
    public static List<PathSegment> generateOptimizedSpiralPath(Geometry safeArea, double laneWidth) {
        if (safeArea == null || safeArea.isEmpty() || !Double.isFinite(laneWidth) || laneWidth <= 0) {
            return Collections.emptyList();
        }
        // 1. æ¸…理几何体,确保有效性
        Geometry working = cleanGeometry(safeArea);
        if (working.isEmpty()) {
            return Collections.emptyList();
        }
        // 2. æå–主多边形(选择面积最大的)
        Polygon mainPolygon = extractMainPolygon(working);
        if (mainPolygon == null) {
            return Collections.emptyList();
        }
        // 3. è®¡ç®—螺旋路径
    List<PathSegment> segments = new ArrayList<>();
    Coordinate cursor = clone(findOptimalStartPoint(mainPolygon));
    Geometry currentLayer = mainPolygon; // start from the outer boundaryand peel inwards
        for (int iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
            Geometry layerGeometry = cleanGeometry(currentLayer);
            if (layerGeometry == null || layerGeometry.isEmpty()) {
                break;
            }
            List<Polygon> polygons = extractPolygons(layerGeometry);
            if (polygons.isEmpty()) {
                break;
            }
            polygons.sort(Comparator.comparingDouble(Polygon::getArea).reversed());
            try {
                for (Polygon polygon : polygons) {
                    LinearRing outer = polygon.getExteriorRing();
                    if (outer == null || outer.getNumPoints() < 4) {
                        continue;
                    }
                    cursor = processRing(outer.getCoordinates(), true, cursor, segments);
                    for (int holeIndex = 0; holeIndex < polygon.getNumInteriorRing(); holeIndex++) {
                        LinearRing hole = polygon.getInteriorRingN(holeIndex);
                        if (hole == null || hole.getNumPoints() < 4) {
                            continue;
                        }
                        cursor = processRing(hole.getCoordinates(), false, cursor, segments);
                    }
                }
            } catch (TopologyException ex) {
                break;
            }
            if (!canShrinkFurther(polygons, laneWidth)) {
                break;
            }
            Geometry nextLayer;
            try {
                nextLayer = layerGeometry.buffer(
                    -laneWidth,
                    BufferParameters.DEFAULT_QUADRANT_SEGMENTS,
                    BufferParameters.CAP_FLAT
                );
            } catch (TopologyException ex) {
                break;
            }
            if (nextLayer.isEmpty() || nextLayer.getArea() < AREA_EPSILON) {
                break;
            }
            double areaDelta = Math.abs(layerGeometry.getArea() - nextLayer.getArea());
            if (areaDelta <= AREA_EPSILON) {
                break;
            }
            currentLayer = nextLayer;
        }
        // 4. ä¼˜åŒ–路径连接
        optimizePathConnections(segments);
        // 5. æ ‡è®°ç«¯ç‚¹
        markEndpoints(segments);
        return segments;
    }
    /**
     * Backward compatible entry that delegates to the optimized implementation.
     */
    public static List<PathSegment> generateSpiralPath(Geometry safeArea, double laneWidth) {
        return generateOptimizedSpiralPath(safeArea, laneWidth);
    }
    /**
     * æ¸…理几何体,确保有效性
     */
    private static Geometry cleanGeometry(Geometry geometry) {
        if (geometry == null) return null;
        try {
            return geometry.buffer(0.0);
        } catch (Exception e) {
            return geometry;
        }
    }
    /**
     * æå–主多边形(面积最大的)
     */
    private static Polygon extractMainPolygon(Geometry geometry) {
        List<Polygon> polygons = extractPolygons(geometry);
        if (polygons.isEmpty()) return null;
        // æŒ‰é¢ç§¯æŽ’序,选择最大的
        polygons.sort((p1, p2) -> Double.compare(p2.getArea(), p1.getArea()));
        return polygons.get(0);
    }
    /**
     * å¯»æ‰¾æœ€ä¼˜èµ·ç‚¹ï¼ˆç¦»å¤šè¾¹å½¢ä¸­å¿ƒæœ€è¿‘的点)
     */
    private static Coordinate findOptimalStartPoint(Polygon polygon) {
        if (polygon == null) return null;
        Coordinate center = polygon.getCentroid().getCoordinate();
        LinearRing ring = polygon.getExteriorRing();
        Coordinate[] coords = ring.getCoordinates();
        Coordinate nearest = coords[0];
        double minDist = Double.MAX_VALUE;
        for (Coordinate coord : coords) {
            double dist = coord.distance(center);
            if (dist < minDist) {
                minDist = dist;
                nearest = coord;
            }
        }
        return nearest;
    }
    /**
     * ä¼˜åŒ–路径连接,减少空驶距离
     */
    private static void optimizePathConnections(List<PathSegment> segments) {
        if (segments == null || segments.size() < 2) {
            return;
        }
        List<PathSegment> optimized = new ArrayList<>(segments.size());
        PathSegment previous = null;
        for (PathSegment segment : segments) {
            if (segment == null || segment.start == null || segment.end == null) {
                continue;
            }
            if (isDegenerate(segment)) {
                continue;
            }
            if (previous != null
                && previous.isMowing == segment.isMowing
                && equals2D(previous.start, segment.start)
                && equals2D(previous.end, segment.end)) {
                continue; // è·³è¿‡é‡å¤æ®µ
            }
            optimized.add(segment);
            previous = segment;
        }
        segments.clear();
        segments.addAll(optimized);
    }
    /**
     * æ£€æŸ¥æ˜¯å¦å¯ä»¥ç»§ç»­ç¼“冲
     */
    private static boolean canShrinkFurther(List<Polygon> polygons, double bufferDistance) {
        if (polygons == null || polygons.isEmpty()) {
            return false;
        }
        for (Polygon polygon : polygons) {
            if (polygon == null || polygon.isEmpty()) {
                continue;
            }
            double width = polygon.getEnvelopeInternal().getWidth();
            double height = polygon.getEnvelopeInternal().getHeight();
            double minDimension = Math.min(width, height);
            if (minDimension <= bufferDistance * 2 * MIN_BUFFER_RATIO) {
                return false;
            }
        }
        return true;
    }
    /**
     * æ ‡è®°èµ·ç‚¹å’Œç»ˆç‚¹
     */
    private static void markEndpoints(List<PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return;
        }
        // å¯»æ‰¾ç¬¬ä¸€ä¸ªå‰²è‰æ®µä½œä¸ºèµ·ç‚¹
        PathSegment firstMowing = null;
        for (PathSegment seg : segments) {
            if (seg != null && seg.isMowing) {
                firstMowing = seg;
                break;
            }
        }
        // å¯»æ‰¾æœ€åŽä¸€ä¸ªå‰²è‰æ®µä½œä¸ºç»ˆç‚¹
        PathSegment lastMowing = null;
        for (int i = segments.size() - 1; i >= 0; i--) {
            PathSegment seg = segments.get(i);
            if (seg != null && seg.isMowing) {
                lastMowing = seg;
                break;
            }
        }
        if (firstMowing != null) {
            firstMowing.setAsStartPoint();
        }
        if (lastMowing != null && lastMowing != firstMowing) {
            lastMowing.setAsEndPoint();
        }
    }
    /**
     * æ£€æŸ¥çº¿æ®µæ˜¯å¦é€€åŒ–(长度过小)
     */
    private static boolean isDegenerate(PathSegment segment) {
        if (segment == null || segment.start == null || segment.end == null) {
            return true;
        }
        double dx = segment.start.x - segment.end.x;
        double dy = segment.start.y - segment.end.y;
        return Math.hypot(dx, dy) <= LENGTH_EPSILON;
    }
    /**
     * æå–多边形(与原方法相同)
     */
    private static List<Polygon> extractPolygons(Geometry geometry) {
        if (geometry == null || geometry.isEmpty()) {
            return Collections.emptyList();
        }
        List<Polygon> result = new ArrayList<>();
        if (geometry instanceof Polygon) {
            result.add((Polygon) geometry);
        } else if (geometry instanceof org.locationtech.jts.geom.MultiPolygon) {
            org.locationtech.jts.geom.MultiPolygon mp = (org.locationtech.jts.geom.MultiPolygon) geometry;
            for (int i = 0; i < mp.getNumGeometries(); i++) {
                Geometry g = mp.getGeometryN(i);
                if (g instanceof Polygon) {
                    result.add((Polygon) g);
                }
            }
        } else if (geometry instanceof org.locationtech.jts.geom.GeometryCollection) {
            org.locationtech.jts.geom.GeometryCollection gc = (org.locationtech.jts.geom.GeometryCollection) geometry;
            for (int i = 0; i < gc.getNumGeometries(); i++) {
                Geometry child = gc.getGeometryN(i);
                result.addAll(extractPolygons(child));
            }
        }
        return result;
    }
    /**
     * å¤åˆ¶åæ ‡
     */
    private static Coordinate clone(Coordinate source) {
        return source == null ? null : new Coordinate(source.x, source.y);
    }
    /**
     * æ¯”较两个坐标是否相同(2D)
     */
    private static boolean equals2D(Coordinate a, Coordinate b) {
        if (a == b) return true;
        if (a == null || b == null) return false;
        return a.distance(b) <= LENGTH_EPSILON;
    }
    private static Coordinate processRing(Coordinate[] coords,
                                          boolean forward,
                                          Coordinate cursor,
                                          List<PathSegment> segments) {
        if (coords == null || coords.length < 4) {
            return cursor;
        }
        List<Coordinate> base = new ArrayList<>(coords.length - 1);
        for (int i = 0; i < coords.length - 1; i++) {
            Coordinate cloned = clone(coords[i]);
            if (cloned != null) {
                base.add(cloned);
            }
        }
        if (base.size() < 2) {
            return cursor;
        }
        if (!forward) {
            Collections.reverse(base);
        }
        int startIndex = 0;
        if (cursor != null) {
            startIndex = findNearestIndex(base, cursor);
        }
        List<Coordinate> ordered = new ArrayList<>(base.size());
        for (int i = 0; i < base.size(); i++) {
            int index = (startIndex + i) % base.size();
            ordered.add(clone(base.get(index)));
        }
        Coordinate firstCoord = ordered.get(0);
        if (cursor != null && !equals2D(cursor, firstCoord)) {
            PathSegment transfer = new PathSegment(clone(cursor), clone(firstCoord), false);
            if (!isDegenerate(transfer)) {
                segments.add(transfer);
            }
        }
        for (int i = 0; i < ordered.size(); i++) {
            Coordinate start = ordered.get(i);
            Coordinate end = ordered.get((i + 1) % ordered.size());
            if (equals2D(start, end)) {
                continue;
            }
            PathSegment mowing = new PathSegment(clone(start), clone(end), true);
            segments.add(mowing);
        }
        return clone(firstCoord);
    }
    private static int findNearestIndex(List<Coordinate> coordinates, Coordinate reference) {
        if (coordinates == null || coordinates.isEmpty() || reference == null) {
            return 0;
        }
        double bestDistance = Double.MAX_VALUE;
        int bestIndex = 0;
        for (int i = 0; i < coordinates.size(); i++) {
            Coordinate candidate = coordinates.get(i);
            if (candidate == null) {
                continue;
            }
            double distance = reference.distance(candidate);
            if (distance < bestDistance) {
                bestDistance = distance;
                bestIndex = i;
            }
        }
        return bestIndex;
    }
}
src/set/Sets.java
@@ -30,6 +30,7 @@
    
    // è®¾ç½®é¡¹ç»„ä»¶
    private JLabel mowerIdLabel;
    private JLabel baseStationIdLabel;
    private JLabel handheldMarkerLabel;
    private JLabel simCardNumberLabel;
    private JLabel baseStationSimLabel;
@@ -38,6 +39,7 @@
    private JLabel idleTrailDurationLabel;
    
    private JButton mowerIdEditBtn;
    private JButton baseStationIdEditBtn;
    private JButton handheldEditBtn;
    private JButton checkUpdateBtn;
    private JButton systemDebugButton;
@@ -105,6 +107,11 @@
            setData.getMowerId() != null ? setData.getMowerId() : "未设置", true);
        mowerIdLabel = (JLabel) mowerIdPanel.getClientProperty("valueLabel");
        mowerIdEditBtn = (JButton) mowerIdPanel.getClientProperty("editButton");
        JPanel baseStationIdPanel = createSettingItemPanel("差分基准站编号",
            resolveBaseStationId(), true);
        baseStationIdLabel = (JLabel) baseStationIdPanel.getClientProperty("valueLabel");
        baseStationIdEditBtn = (JButton) baseStationIdPanel.getClientProperty("editButton");
        
        JPanel handheldPanel = createSettingItemPanel("便携打点器编号",
            setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "未设置", true);
@@ -135,7 +142,8 @@
        // APP版本
        JPanel appVersionPanel = createAppVersionPanel();
        
        addRowWithSpacing(panel, mowerIdPanel);
    addRowWithSpacing(panel, mowerIdPanel);
    addRowWithSpacing(panel, baseStationIdPanel);
        addRowWithSpacing(panel, handheldPanel);
    addRowWithSpacing(panel, simCardPanel);
    addRowWithSpacing(panel, baseStationSimPanel);
@@ -432,6 +440,10 @@
            mowerIdLabel.setText(setData.getMowerId() != null ? setData.getMowerId() : "未设置");
        }
        if (baseStationIdLabel != null) {
            baseStationIdLabel.setText(resolveBaseStationId());
        }
        if (handheldMarkerLabel != null) {
            handheldMarkerLabel.setText(setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "未设置");
        }
@@ -477,12 +489,31 @@
        }
        return trimmed;
    }
    private String resolveBaseStationId() {
        if (baseStation == null) {
            return "未设置";
        }
        String value = baseStation.getDeviceId();
        if (value == null) {
            return "未设置";
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return "未设置";
        }
        return trimmed;
    }
    
    private void setupEventHandlers() {
        // å‰²è‰æœºç¼–号编辑按钮事件
        if (mowerIdEditBtn != null) {
            mowerIdEditBtn.addActionListener(e -> editMowerId());
        }
        if (baseStationIdEditBtn != null) {
            baseStationIdEditBtn.addActionListener(e -> editBaseStationId());
        }
        
        // æ£€æŸ¥æ›´æ–°æŒ‰é’®äº‹ä»¶
        if (checkUpdateBtn != null) {
@@ -559,6 +590,45 @@
        }
    }
    private void editBaseStationId() {
        String currentValue = "未设置".equals(resolveBaseStationId()) ? "" : resolveBaseStationId();
        String newValue = (String) JOptionPane.showInputDialog(this,
                "请输入差分基准站编号:",
                "修改差分基准站编号",
                JOptionPane.QUESTION_MESSAGE,
                null,
                null,
                currentValue);
        if (newValue == null) {
            return;
        }
        newValue = newValue.trim();
        if (newValue.isEmpty()) {
            JOptionPane.showMessageDialog(this, "差分基准站编号不能为空", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        try {
            baseStation.updateByDeviceId(newValue,
                    baseStation.getInstallationCoordinates(),
                    baseStation.getIotSimCardNumber(),
                    baseStation.getDeviceActivationTime(),
                    baseStation.getDataUpdateTime());
            baseStation.load();
            if (baseStationIdLabel != null) {
                baseStationIdLabel.setText(resolveBaseStationId());
            }
            JOptionPane.showMessageDialog(this, "差分基准站编号更新成功", "成功", JOptionPane.INFORMATION_MESSAGE);
        } catch (IllegalArgumentException ex) {
            JOptionPane.showMessageDialog(this, ex.getMessage(), "输入错误", JOptionPane.WARNING_MESSAGE);
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "差分基准站编号更新失败", "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    private void editIdleTrailDuration() {
        int currentSeconds = setData != null ? setData.getIdleTrailDurationSeconds() : Setsys.DEFAULT_IDLE_TRAIL_DURATION_SECONDS;
        if (currentSeconds <= 0) {
src/zhangaiwu/AddDikuai.java
@@ -14,16 +14,17 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Comparator;
import java.awt.geom.Point2D;
import baseStation.BaseStation;
import bianjie.jisuanmianjie;
import dikuai.Dikuai;
import dikuai.Dikuaiguanli;
import gecaoji.Device;
import bianjie.bianjieguihua2;
import lujing.Lunjingguihua;
import ui.UIConfig;
@@ -49,6 +50,9 @@
    private final Color LIGHT_TEXT = new Color(108, 117, 125);
    private final Color BORDER_COLOR = new Color(222, 226, 230);
    private final Color SUCCESS_COLOR = new Color(40, 167, 69);
    private final Color ERROR_COLOR = new Color(220, 53, 69);
    private static final String KEY_PATH_MESSAGE_TEXT = "__pathMessageText";
    private static final String KEY_PATH_MESSAGE_SUCCESS = "__pathMessageSuccess";
    
    // æ­¥éª¤é¢æ¿
    private JPanel mainPanel;
@@ -68,8 +72,12 @@
    private JButton prevButton;
    private JButton nextButton;
    private JButton createButton;
    private JButton previewButton;
    private Component previewButtonSpacer;
    private JLabel boundaryCountLabel;
    private JPanel obstacleListContainer;
    private JTextArea pathGenerationMessageArea;
    private JPanel pathMessageWrapper;
    
    // åœ°å—数据
    private Map<String, String> dikuaiData = new HashMap<>();
@@ -967,6 +975,29 @@
        stepPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        stepPanel.add(generatePathButton);
        stepPanel.add(Box.createRigidArea(new Dimension(0, 12)));
        pathMessageWrapper = new JPanel(new BorderLayout());
        pathMessageWrapper.setAlignmentX(Component.LEFT_ALIGNMENT);
        pathMessageWrapper.setBackground(PRIMARY_LIGHT);
        pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(PRIMARY_COLOR, 1),
            BorderFactory.createEmptyBorder(12, 12, 12, 12)
        ));
        pathMessageWrapper.setVisible(false);
        pathGenerationMessageArea = new JTextArea();
        pathGenerationMessageArea.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        pathGenerationMessageArea.setForeground(TEXT_COLOR);
        pathGenerationMessageArea.setOpaque(false);
        pathGenerationMessageArea.setEditable(false);
        pathGenerationMessageArea.setLineWrap(true);
        pathGenerationMessageArea.setWrapStyleWord(true);
        pathGenerationMessageArea.setFocusable(false);
        pathGenerationMessageArea.setBorder(null);
        pathMessageWrapper.add(pathGenerationMessageArea, BorderLayout.CENTER);
        stepPanel.add(pathMessageWrapper);
        stepPanel.add(Box.createVerticalGlue());
        return stepPanel;
@@ -1026,6 +1057,9 @@
    private void generateMowingPath() {
        if (!dikuaiData.containsKey("boundaryDrawn")) {
            JOptionPane.showMessageDialog(this, "请先完成边界绘制后再生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("请先完成边界绘制后再生成路径。", false);
            setPathAvailability(false);
            showStep(2);
            return;
        }
@@ -1039,9 +1073,9 @@
        }
        if (boundaryCoords == null) {
            JOptionPane.showMessageDialog(this, "未找到有效的地块边界坐标,无法生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("未找到有效的地块边界坐标,无法生成路径。", false);
            setPathAvailability(false);
            return;
        }
@@ -1060,17 +1094,17 @@
        Object widthObj = mowingWidthSpinner.getValue();
        if (!(widthObj instanceof Number)) {
            JOptionPane.showMessageDialog(this, "割草宽度输入无效", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草宽度输入无效,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        double widthCm = ((Number) widthObj).doubleValue();
        if (widthCm <= 0) {
            JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草宽度必须大于0,请重新设置。", false);
            setPathAvailability(false);
            return;
        }
        dikuaiData.put("mowingWidth", widthObj.toString());
@@ -1088,31 +1122,141 @@
            String plannedPath = Lunjingguihua.formatPathSegments(segments);
            if (!isMeaningfulValue(plannedPath)) {
                JOptionPane.showMessageDialog(this, "生成割草路径失败: ç”Ÿæˆç»“果为空", "错误", JOptionPane.ERROR_MESSAGE);
                if (createButton != null) {
                    createButton.setEnabled(false);
                }
                dikuaiData.remove("plannedPath");
                showPathGenerationMessage("生成割草路径失败:生成结果为空。", false);
                setPathAvailability(false);
                return;
            }
            dikuaiData.put("plannedPath", plannedPath);
            if (createButton != null) {
                createButton.setEnabled(true);
            if (isMeaningfulValue(boundaryCoords)) {
                dikuaiData.put("boundaryCoordinates", boundaryCoords);
            }
            JOptionPane.showMessageDialog(this,
                "已根据当前设置生成割草路径,共生成 " + segments.size() + " æ®µã€‚",
                "成功",
                JOptionPane.INFORMATION_MESSAGE);
            if (isMeaningfulValue(obstacleCoords)) {
                dikuaiData.put("obstacleCoordinates", obstacleCoords);
            }
            dikuaiData.put("plannedPath", plannedPath);
            setPathAvailability(true);
            showPathGenerationMessage(
                "已根据当前设置生成割草路径,共生成 " + segments.size() + " æ®µã€‚\n点击“预览”按钮可在主页面查看效果。",
                true);
        } catch (IllegalArgumentException ex) {
            JOptionPane.showMessageDialog(this, "生成割草路径失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("生成割草路径失败:" + ex.getMessage(), false);
            setPathAvailability(false);
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "生成割草路径时发生异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("生成割草路径时发生异常:" + ex.getMessage(), false);
            setPathAvailability(false);
        }
    }
    private void previewMowingPath() {
        if (!hasGeneratedPath()) {
            showPathGenerationMessage("请先生成割草路径后再预览。", false);
            setPathAvailability(false);
            return;
        }
        persistStep3Inputs();
        String landNumber = getPendingLandNumber();
        String trimmedAreaName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        String displayAreaName = isMeaningfulValue(trimmedAreaName) ? trimmedAreaName : landNumber;
        String plannedPath = dikuaiData.get("plannedPath");
        if (!isMeaningfulValue(plannedPath)) {
            showPathGenerationMessage("请先生成割草路径后再预览。", false);
            setPathAvailability(false);
            return;
        }
        String boundary = null;
        Dikuai pending = getOrCreatePendingDikuai();
        if (pending != null) {
            boundary = normalizeCoordinateValue(pending.getBoundaryCoordinates());
        }
        if (boundary == null) {
            boundary = normalizeCoordinateValue(dikuaiData.get("boundaryCoordinates"));
        }
        String obstacles = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates"));
        if (!isMeaningfulValue(obstacles)) {
            obstacles = resolveObstaclePayloadFromConfig(landNumber);
            if (isMeaningfulValue(obstacles)) {
                dikuaiData.put("obstacleCoordinates", obstacles);
            }
        }
        Shouye shouye = Shouye.getInstance();
        if (shouye == null) {
            JOptionPane.showMessageDialog(this, "无法打开主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        dikuaiData.put("areaName", trimmedAreaName);
        if (isMeaningfulValue(boundary)) {
            dikuaiData.put("boundaryCoordinates", boundary);
        }
        pendingLandNumber = landNumber;
        captureSessionSnapshot();
        resumeRequested = true;
        boolean started = shouye.startMowingPathPreview(
            landNumber,
            displayAreaName,
            boundary,
            obstacles,
            plannedPath,
            AddDikuai::resumeFromPreview
        );
        if (!started) {
            resumeRequested = false;
            JOptionPane.showMessageDialog(this, "无法启动预览,请稍后再试", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        closePreviewAndDispose();
    }
    private void persistStep3Inputs() {
        String trimmedName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        dikuaiData.put("areaName", trimmedName);
        if (mowingPatternCombo != null) {
            Object selection = mowingPatternCombo.getSelectedItem();
            if (selection != null) {
                dikuaiData.put("mowingPattern", selection.toString());
            }
        }
        if (mowingWidthSpinner != null) {
            Object widthValue = mowingWidthSpinner.getValue();
            if (widthValue instanceof Number) {
                int widthInt = ((Number) widthValue).intValue();
                dikuaiData.put("mowingWidth", Integer.toString(widthInt));
            } else if (widthValue != null) {
                dikuaiData.put("mowingWidth", widthValue.toString());
            }
        }
    }
    private void captureSessionSnapshot() {
        if (activeSession == null) {
            activeSession = new DrawingSession();
        }
        String landNumber = getPendingLandNumber();
        activeSession.landNumber = landNumber;
        activeSession.areaName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        activeSession.drawingCompleted = true;
        activeSession.data = new HashMap<>(dikuaiData);
    }
    private void closePreviewAndDispose() {
        setVisible(false);
        dispose();
    }
    
    private JButton createPrimaryButton(String text, int fontSize) {
@@ -1146,6 +1290,44 @@
        
        return button;
    }
    private void showPathGenerationMessage(String message, boolean success) {
        if (pathGenerationMessageArea == null || pathMessageWrapper == null) {
            return;
        }
        String display = message == null ? "" : message.trim();
        if (display.isEmpty()) {
            dikuaiData.remove(KEY_PATH_MESSAGE_TEXT);
            dikuaiData.remove(KEY_PATH_MESSAGE_SUCCESS);
        } else {
            dikuaiData.put(KEY_PATH_MESSAGE_TEXT, display);
            dikuaiData.put(KEY_PATH_MESSAGE_SUCCESS, success ? "true" : "false");
        }
        pathGenerationMessageArea.setText(display);
        Color borderColor = success ? PRIMARY_COLOR : ERROR_COLOR;
        Color textColor = success ? PRIMARY_DARK : ERROR_COLOR;
        Color backgroundColor = success ? PRIMARY_LIGHT : new Color(255, 235, 238);
        pathGenerationMessageArea.setForeground(textColor);
        pathMessageWrapper.setBackground(backgroundColor);
        pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(borderColor, 1),
            BorderFactory.createEmptyBorder(12, 12, 12, 12)
        ));
        pathMessageWrapper.setVisible(!display.isEmpty());
        pathMessageWrapper.revalidate();
        pathMessageWrapper.repaint();
    }
    private void setPathAvailability(boolean available) {
        boolean effective = available && currentStep == 3;
        if (createButton != null) {
            createButton.setEnabled(effective);
        }
        if (previewButton != null) {
            boolean visible = previewButton.isVisible();
            previewButton.setEnabled(effective && visible);
        }
    }
    
    private JPanel createButtonPanel() {
        JPanel buttonPanel = new JPanel();
@@ -1167,11 +1349,20 @@
        nextButton = createPrimaryButton("下一步", 16);
        createButton = createPrimaryButton("保存", 16);
        createButton.setVisible(false);
    createButton.setEnabled(false);
        createButton.setEnabled(false);
        previewButton = createPrimaryButton("预览", 16);
        previewButton.setVisible(false);
        previewButton.setEnabled(false);
        previewButtonSpacer = Box.createHorizontalStrut(15);
        previewButtonSpacer.setVisible(false);
        buttonPanel.add(prevButton);
        buttonPanel.add(Box.createHorizontalGlue());
        buttonPanel.add(nextButton);
        buttonPanel.add(previewButtonSpacer);
        buttonPanel.add(previewButton);
        buttonPanel.add(Box.createHorizontalStrut(15));
        buttonPanel.add(createButton);
@@ -1223,14 +1414,40 @@
        return true;
    }
    private static String buildOriginalBoundaryString() {
        if (Coordinate.coordinates == null || Coordinate.coordinates.isEmpty()) {
    private static List<Coordinate> sanitizeCoordinateList(List<Coordinate> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<Coordinate> snapshot = new ArrayList<>();
        for (Coordinate coordinate : source) {
            if (coordinate != null) {
                snapshot.add(coordinate);
            }
        }
        if (snapshot.isEmpty()) {
            return Collections.emptyList();
        }
        DecimalFormat latLonFormat = new DecimalFormat("0.000000");
        LinkedHashMap<String, Coordinate> unique = new LinkedHashMap<>();
        for (Coordinate coord : snapshot) {
            double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection());
            double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection());
            String key = latLonFormat.format(lat) + "," + latLonFormat.format(lon);
            unique.putIfAbsent(key, coord);
        }
        return new ArrayList<>(unique.values());
    }
    private static String buildOriginalBoundaryString(List<Coordinate> coordinates) {
        if (coordinates == null || coordinates.isEmpty()) {
            return "-1";
        }
        StringBuilder sb = new StringBuilder();
        DecimalFormat latLonFormat = new DecimalFormat("0.000000");
        DecimalFormat elevationFormat = new DecimalFormat("0.00");
        for (Coordinate coord : Coordinate.coordinates) {
        for (Coordinate coord : coordinates) {
            double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection());
            double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection());
            double elevation = coord.getElevation();
@@ -1334,8 +1551,14 @@
    }
    private static BoundarySnapshotResult computeBoundarySnapshot() {
        int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0;
        if (count < 3) {
        List<Coordinate> uniqueCoordinates;
        synchronized (Coordinate.coordinates) {
            uniqueCoordinates = sanitizeCoordinateList(Coordinate.coordinates);
            Coordinate.coordinates.clear();
            Coordinate.coordinates.addAll(uniqueCoordinates);
        }
        if (uniqueCoordinates.size() < 3) {
            return BoundarySnapshotResult.failure("采集的边界点不足,无法生成地块边界", JOptionPane.WARNING_MESSAGE);
        }
@@ -1344,9 +1567,9 @@
            return BoundarySnapshotResult.failure("当前地块面积为0,无法继续", JOptionPane.WARNING_MESSAGE);
        }
        Device device = new Device();
        device.initFromProperties();
        String baseStationCoordinates = normalizeCoordinateValue(device.getBaseStationCoordinates());
    BaseStation baseStation = new BaseStation();
    baseStation.load();
    String baseStationCoordinates = normalizeCoordinateValue(baseStation.getInstallationCoordinates());
        if (!isMeaningfulValue(baseStationCoordinates)) {
            return BoundarySnapshotResult.failure("未获取到有效的基准站坐标,请先在基准站管理中设置", JOptionPane.WARNING_MESSAGE);
        }
@@ -1359,7 +1582,7 @@
            return BoundarySnapshotResult.failure("生成地块边界失败: " + ex.getMessage(), JOptionPane.ERROR_MESSAGE);
        }
        String originalBoundary = buildOriginalBoundaryString();
    String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates);
        DecimalFormat areaFormat = new DecimalFormat("0.00");
        String areaString = areaFormat.format(area);
@@ -1407,6 +1630,10 @@
        
        // åˆ›å»ºåœ°å—按钮
        createButton.addActionListener(e -> createDikuai());
        if (previewButton != null) {
            previewButton.addActionListener(e -> previewMowingPath());
        }
        
        // å…³é—­å¯¹è¯æ¡†
        addWindowListener(new WindowAdapter() {
@@ -1435,11 +1662,24 @@
        if (step < 3) {
            nextButton.setVisible(true);
            createButton.setVisible(false);
            createButton.setEnabled(false);
            setPathAvailability(false);
            if (previewButton != null) {
                previewButton.setVisible(false);
                previewButton.setEnabled(false);
            }
            if (previewButtonSpacer != null) {
                previewButtonSpacer.setVisible(false);
            }
        } else {
            nextButton.setVisible(false);
            createButton.setVisible(true);
            createButton.setEnabled(hasGeneratedPath());
            if (previewButton != null) {
                previewButton.setVisible(true);
            }
            if (previewButtonSpacer != null) {
                previewButtonSpacer.setVisible(true);
            }
            setPathAvailability(hasGeneratedPath());
        }
        Container parent = prevButton.getParent();
@@ -1736,6 +1976,71 @@
            showStep(1);
            hideBoundaryPointSummary();
        }
        restoreGeneratedPathState(session);
    }
    private void restoreGeneratedPathState(DrawingSession session) {
        if (session == null || session.data == null) {
            showPathGenerationMessage("", true);
            return;
        }
        Map<String, String> data = session.data;
        if (mowingPatternCombo != null) {
            String pattern = data.get("mowingPattern");
            if (pattern != null) {
                ComboBoxModel<String> model = mowingPatternCombo.getModel();
                for (int i = 0; i < model.getSize(); i++) {
                    String candidate = model.getElementAt(i);
                    if (pattern.equals(candidate)) {
                        mowingPatternCombo.setSelectedIndex(i);
                        break;
                    }
                }
            }
        }
        if (mowingWidthSpinner != null) {
            String width = data.get("mowingWidth");
            if (isMeaningfulValue(width)) {
                try {
                    double parsed = Double.parseDouble(width.trim());
                    SpinnerNumberModel model = (SpinnerNumberModel) mowingWidthSpinner.getModel();
                    int min = ((Number) model.getMinimum()).intValue();
                    int max = ((Number) model.getMaximum()).intValue();
                    int rounded = (int) Math.round(parsed);
                    if (rounded < min) {
                        rounded = min;
                    } else if (rounded > max) {
                        rounded = max;
                    }
                    mowingWidthSpinner.setValue(rounded);
                } catch (NumberFormatException ignored) {
                    // ä¿æŒå½“前值
                }
            }
        }
        boolean hasPath = isMeaningfulValue(data.get("plannedPath"));
        if (!hasPath) {
            showPathGenerationMessage("", true);
            if (currentStep == 3) {
                setPathAvailability(false);
            }
            return;
        }
        String message = data.get(KEY_PATH_MESSAGE_TEXT);
        boolean success = !"false".equalsIgnoreCase(data.get(KEY_PATH_MESSAGE_SUCCESS));
        showStep(3);
        if (isMeaningfulValue(message)) {
            showPathGenerationMessage(message, success);
        } else {
            showPathGenerationMessage("已生成割草路径,可点击“预览”按钮查看效果。", true);
        }
        setPathAvailability(true);
    }
    public static void finishDrawingSession() {
@@ -1769,6 +2074,19 @@
        Component parent = shouye != null ? shouye : null;
        showAddDikuaiDialog(parent);
    }
    public static void resumeFromPreview() {
        Shouye shouye = Shouye.getInstance();
        if (shouye != null) {
            shouye.exitMowingPathPreview();
        }
        if (activeSession == null) {
            return;
        }
        resumeRequested = true;
        Component parent = shouye != null ? shouye : null;
        SwingUtilities.invokeLater(() -> showAddDikuaiDialog(parent));
    }
    
    private void createDikuai() {
        if (!validateCurrentStep()) {
src/zhuye/HandheldBoundaryCaptureDialog.java
ÎļþÒÑɾ³ý
src/zhuye/MapRenderer.java
@@ -50,6 +50,7 @@
    private static final double CIRCLE_SAMPLE_SIZE = 0.54d;
    private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05;
    private static final double BOUNDARY_CONTAINS_TOLERANCE = 0.05;
    private static final double PREVIEW_BOUNDARY_MARKER_SCALE = 0.25d;
    
    // ç»„件引用
    private JPanel visualizationPanel;
@@ -64,6 +65,8 @@
    private String currentObstacleLandNumber;
    private String boundaryName;
    private boolean boundaryPointsVisible;
    private double boundaryPointSizeScale = 1.0d;
    private boolean previewSizingEnabled;
    private String currentBoundaryLandNumber;
    private boolean dragInProgress;
    private final Gecaoji mower;
@@ -74,6 +77,7 @@
    private final List<Point2D.Double> realtimeMowingTrack = new ArrayList<>();
    private final Deque<tuowei.TrailSample> idleMowerTrail = new ArrayDeque<>();
    private final List<Point2D.Double> handheldBoundaryPreview = new ArrayList<>();
    private double boundaryPreviewMarkerScale = 1.0d;
    private boolean realtimeTrackRecording;
    private String realtimeTrackLandNumber;
    private double mowerEffectiveWidthMeters;
@@ -264,19 +268,21 @@
            drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale);
        }
        adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive);
    adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive, boundaryPreviewMarkerScale);
        if (hasPlannedPath) {
            drawCurrentPlannedPath(g2d);
        }
        if (boundaryPointsVisible && hasBoundary) {
            double markerScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d);
            pointandnumber.drawBoundaryPoints(
                g2d,
                currentBoundary,
                scale,
                BOUNDARY_POINT_MERGE_THRESHOLD,
                BOUNDARY_POINT_COLOR
                BOUNDARY_POINT_COLOR,
                markerScale
            );
        }
@@ -648,6 +654,10 @@
        visualizationPanel.repaint();
    }
    public void clearIdleTrail() {
        clearIdleMowerTrail();
    }
    public void setIdleTrailDurationSeconds(int seconds) {
        int sanitized = seconds;
        if (sanitized < 5 || sanitized > 600) {
@@ -865,7 +875,8 @@
    }
    private void drawCurrentPlannedPath(Graphics2D g2d) {
        lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale);
        double arrowScale = previewSizingEnabled ? 0.5d : 1.0d;
        lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale, arrowScale);
    }
    private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) {
@@ -1054,7 +1065,11 @@
    private double computeSelectionThresholdPixels() {
        double scaleFactor = Math.max(0.5, scale);
        double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2);
        double diameterScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d);
        if (!Double.isFinite(diameterScale) || diameterScale <= 0.0d) {
            diameterScale = 1.0d;
        }
        double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2 * diameterScale);
        double markerDiameterPixels = markerDiameterWorld * scale;
        return Math.max(8.0, markerDiameterPixels * 1.5);
    }
@@ -1830,6 +1845,65 @@
        visualizationPanel.repaint();
    }
    public void setBoundaryPointSizeScale(double sizeScale) {
        double normalized = (Double.isFinite(sizeScale) && sizeScale > 0.0d) ? sizeScale : 1.0d;
        if (Math.abs(boundaryPointSizeScale - normalized) < 1e-6) {
            return;
        }
        boundaryPointSizeScale = normalized;
        if (visualizationPanel == null) {
            return;
        }
        if (SwingUtilities.isEventDispatchThread()) {
            visualizationPanel.repaint();
        } else {
            SwingUtilities.invokeLater(visualizationPanel::repaint);
        }
    }
    public void setPathPreviewSizingEnabled(boolean enabled) {
        previewSizingEnabled = enabled;
        if (visualizationPanel == null) {
            return;
        }
        if (SwingUtilities.isEventDispatchThread()) {
            visualizationPanel.repaint();
        } else {
            SwingUtilities.invokeLater(visualizationPanel::repaint);
        }
    }
    public void setBoundaryPreviewMarkerScale(double markerScale) {
        double normalized = Double.isFinite(markerScale) && markerScale > 0.0d ? markerScale : 1.0d;
        if (Math.abs(boundaryPreviewMarkerScale - normalized) < 1e-6) {
            return;
        }
        boundaryPreviewMarkerScale = normalized;
        if (visualizationPanel == null) {
            return;
        }
        if (SwingUtilities.isEventDispatchThread()) {
            visualizationPanel.repaint();
        } else {
            SwingUtilities.invokeLater(visualizationPanel::repaint);
        }
    }
    public boolean setHandheldMowerIconActive(boolean handheldActive) {
        if (mower == null) {
            return false;
        }
        boolean changed = mower.useHandheldIcon(handheldActive);
        if (changed && visualizationPanel != null) {
            if (SwingUtilities.isEventDispatchThread()) {
                visualizationPanel.repaint();
            } else {
                SwingUtilities.invokeLater(visualizationPanel::repaint);
            }
        }
        return changed;
    }
    public void beginHandheldBoundaryPreview() {
        handheldBoundaryPreviewActive = true;
        handheldBoundaryPreview.clear();
@@ -1859,6 +1933,7 @@
    public void clearHandheldBoundaryPreview() {
        handheldBoundaryPreviewActive = false;
        handheldBoundaryPreview.clear();
        boundaryPreviewMarkerScale = 1.0d;
        visualizationPanel.repaint();
    }
@@ -1914,8 +1989,10 @@
            return;
        }
        double width = Math.max(bounds.width, 1);
        double height = Math.max(bounds.height, 1);
        Rectangle2D.Double targetBounds = includeMowerInBounds(bounds);
        double width = Math.max(targetBounds.width, 1);
        double height = Math.max(targetBounds.height, 1);
        double targetWidth = width * 1.2;
        double targetHeight = height * 1.2;
@@ -1927,8 +2004,41 @@
        newScale = Math.max(0.05, Math.min(newScale, 50.0));
        this.scale = newScale;
        this.translateX = -bounds.getCenterX();
        this.translateY = -bounds.getCenterY();
        this.translateX = -targetBounds.getCenterX();
        this.translateY = -targetBounds.getCenterY();
    }
    // Keep the mower marker inside the viewport whenever the camera refits to scene bounds.
    private Rectangle2D.Double includeMowerInBounds(Rectangle2D.Double bounds) {
        Rectangle2D.Double expanded = new Rectangle2D.Double(
            bounds.x,
            bounds.y,
            Math.max(0.0, bounds.width),
            Math.max(0.0, bounds.height)
        );
        if (mower == null || !mower.hasValidPosition()) {
            return expanded;
        }
        Point2D.Double mowerPosition = mower.getPosition();
        if (mowerPosition == null
            || !Double.isFinite(mowerPosition.x)
            || !Double.isFinite(mowerPosition.y)) {
            return expanded;
        }
        double minX = Math.min(expanded.x, mowerPosition.x);
        double minY = Math.min(expanded.y, mowerPosition.y);
        double maxX = Math.max(expanded.x + expanded.width, mowerPosition.x);
        double maxY = Math.max(expanded.y + expanded.height, mowerPosition.y);
        expanded.x = minX;
        expanded.y = minY;
        expanded.width = Math.max(0.0, maxX - minX);
        expanded.height = Math.max(0.0, maxY - minY);
        return expanded;
    }
    public void dispose() {
src/zhuye/Shouye.java
@@ -92,6 +92,11 @@
    private JPanel floatingButtonPanel;
    private JPanel floatingButtonColumn;
    private Runnable endDrawingCallback;
    private JButton pathPreviewReturnButton;
    private boolean pathPreviewActive;
    private Runnable pathPreviewReturnAction;
    private String previewRestoreLandNumber;
    private String previewRestoreLandName;
    private boolean drawingPaused;
    private ImageIcon pauseIcon;
    private ImageIcon pauseActiveIcon;
@@ -112,7 +117,6 @@
    private double[] circleBaseLatLon;
    private Timer circleDataMonitor;
    private Coordinate lastCapturedCoordinate;
    private HandheldBoundaryCaptureDialog handheldCaptureDialog;
    private boolean handheldCaptureActive;
    private int handheldCapturedPoints;
    private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>();
@@ -127,14 +131,26 @@
    private boolean stopButtonActive = false;
    private boolean bluetoothConnected = false;
    private Timer mowerSpeedRefreshTimer;
    private boolean drawingControlModeActive;
    private boolean storedStartButtonShowingPause;
    private boolean storedStopButtonActive;
    private String storedStatusBeforeDrawing;
    private boolean handheldCaptureInlineUiActive;
    private Timer handheldCaptureStatusTimer;
    private String handheldCaptureStoredStatusText;
    private Color handheldStartButtonOriginalBackground;
    private Color handheldStartButtonOriginalForeground;
    private Color handheldStopButtonOriginalBackground;
    private Color handheldStopButtonOriginalForeground;
    
    public Shouye() {
        instance = this;
        baseStation = new BaseStation();
        baseStation.load();
    dellmessage.registerLineListener(serialLineListener);
        dellmessage.registerLineListener(serialLineListener);
        initializeUI();
        setupEventHandlers();
        scheduleIdentifierCheck();
    }
    public static Shouye getInstance() {
@@ -174,6 +190,26 @@
        refreshMapForSelectedArea();
    }
    private void scheduleIdentifierCheck() {
        HierarchyListener listener = new HierarchyListener() {
            @Override
            public void hierarchyChanged(HierarchyEvent e) {
                if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && Shouye.this.isShowing()) {
                    Shouye.this.removeHierarchyListener(this);
                    SwingUtilities.invokeLater(() -> {
                        Shouye.this.checkIdentifiersAndPromptIfNeeded();
                        Shouye.this.showInitialMowerSelfCheckDialogIfNeeded();
                    });
                }
            }
        };
        addHierarchyListener(listener);
    }
    private void showInitialMowerSelfCheckDialogIfNeeded() {
        zijian.showInitialPromptIfNeeded(this, this::showRemoteControlDialog);
    }
    private void applyIdleTrailDurationFromSettings() {
        if (mapRenderer == null) {
            return;
@@ -584,8 +620,9 @@
        }
        if (remoteDialog != null) {
            positionRemoteDialogBottomCenter(remoteDialog);
            zijian.markSelfCheckCompleted();
            remoteDialog.setVisible(true);
        }
        remoteDialog.setVisible(true);
    }
    private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) {
@@ -663,6 +700,142 @@
        baseStationDialog.setVisible(true);
    }
    private void checkIdentifiersAndPromptIfNeeded() {
        if (baseStation == null) {
            baseStation = new BaseStation();
        }
        baseStation.load();
        String currentMowerId = Setsys.getPropertyValue("mowerId");
        String currentBaseStationId = baseStation.getDeviceId();
        if (!isIdentifierMissing(currentMowerId) && !isIdentifierMissing(currentBaseStationId)) {
            return;
        }
        Window owner = SwingUtilities.getWindowAncestor(this);
        promptForMissingIdentifiers(owner, currentMowerId, currentBaseStationId);
    }
    private void promptForMissingIdentifiers(Window owner, String currentMowerId, String currentBaseStationId) {
        while (true) {
            JTextField mowerField = new JTextField(10);
            JTextField baseField = new JTextField(10);
            if (!isIdentifierMissing(currentMowerId)) {
                mowerField.setText(currentMowerId.trim());
            }
            if (!isIdentifierMissing(currentBaseStationId)) {
                baseField.setText(currentBaseStationId.trim());
            }
            JPanel panel = new JPanel();
            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
            panel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            JLabel mowerLabel = new JLabel("割草机编号");
            JLabel baseLabel = new JLabel("差分基准站编号");
            mowerField.setMaximumSize(new Dimension(Integer.MAX_VALUE, mowerField.getPreferredSize().height));
            baseField.setMaximumSize(new Dimension(Integer.MAX_VALUE, baseField.getPreferredSize().height));
            panel.add(mowerLabel);
            panel.add(Box.createVerticalStrut(4));
            panel.add(mowerField);
            panel.add(Box.createVerticalStrut(10));
            panel.add(baseLabel);
            panel.add(Box.createVerticalStrut(4));
            panel.add(baseField);
            Object[] options = {"保存", "取消"};
            int result = JOptionPane.showOptionDialog(owner, panel, "完善设备信息",
                    JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]);
            if (result != 0) {
                break;
            }
            String mowerInput = mowerField.getText().trim();
            String baseInput = baseField.getText().trim();
            if (mowerInput.isEmpty()) {
                JOptionPane.showMessageDialog(owner, "割草机编号不能为空。", "提示", JOptionPane.WARNING_MESSAGE);
                continue;
            }
            if (baseInput.isEmpty()) {
                JOptionPane.showMessageDialog(owner, "差分基准站编号不能为空。", "提示", JOptionPane.WARNING_MESSAGE);
                continue;
            }
            boolean mowerSaved = persistMowerIdentifier(mowerInput);
            boolean baseSaved = persistBaseStationIdentifier(baseInput);
            if (mowerSaved && baseSaved) {
                JOptionPane.showMessageDialog(owner, "编号已保存。", "成功", JOptionPane.INFORMATION_MESSAGE);
                break;
            }
            StringBuilder errorBuilder = new StringBuilder();
            if (!mowerSaved) {
                errorBuilder.append("割草机编号保存失败。");
            }
            if (!baseSaved) {
                if (errorBuilder.length() > 0) {
                    errorBuilder.append('\n');
                }
                errorBuilder.append("差分基准站编号保存失败。");
            }
            JOptionPane.showMessageDialog(owner, errorBuilder.toString(), "保存失败", JOptionPane.ERROR_MESSAGE);
            currentMowerId = Setsys.getPropertyValue("mowerId");
            baseStation.load();
            currentBaseStationId = baseStation.getDeviceId();
        }
    }
    private boolean isIdentifierMissing(String value) {
        if (value == null) {
            return true;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() || "-1".equals(trimmed);
    }
    private boolean persistMowerIdentifier(String mowerId) {
        try {
            Setsys setsys = new Setsys();
            setsys.initializeFromProperties();
            boolean updated = setsys.updateProperty("mowerId", mowerId);
            if (updated) {
                Device.initializeActiveDevice(mowerId);
            }
            return updated;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    private boolean persistBaseStationIdentifier(String baseStationId) {
        if (baseStation == null) {
            baseStation = new BaseStation();
        }
        try {
            baseStation.updateByDeviceId(baseStationId,
                    baseStation.getInstallationCoordinates(),
                    baseStation.getIotSimCardNumber(),
                    baseStation.getDeviceActivationTime(),
                    baseStation.getDataUpdateTime());
            baseStation.load();
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    private boolean hasValidBaseStationId() {
        if (baseStation == null) {
            return false;
@@ -713,9 +886,22 @@
    }
    
    private void toggleStartPause() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldConfirmAction();
            return;
        }
        if (drawingControlModeActive) {
            toggleDrawingPause();
            return;
        }
        if (startBtn == null) {
            return;
        }
        if (startButtonShowingPause) {
            if (!zijian.ensureBeforeMowing(this, this::showRemoteControlDialog)) {
                return;
            }
        }
        startButtonShowingPause = !startButtonShowingPause;
        if (!startButtonShowingPause) {
            statusLabel.setText("作业中");
@@ -737,6 +923,14 @@
    }
    private void handleStopAction() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldFinishAction();
            return;
        }
        if (drawingControlModeActive) {
            handleDrawingStopFromControlPanel();
            return;
        }
        stopButtonActive = !stopButtonActive;
        updateStopButtonIcon();
        if (stopButtonActive) {
@@ -751,6 +945,268 @@
        updateStartButtonAppearance();
    }
    private void handleDrawingStopFromControlPanel() {
        if (endDrawingCallback != null) {
            endDrawingCallback.run();
        } else {
            addzhangaiwu.finishDrawingSession();
        }
    }
    private void handleHandheldConfirmAction() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        if (!canConfirmHandheldPoint()) {
            refreshHandheldCaptureUiState();
            return;
        }
        int count = captureHandheldBoundaryPoint();
        if (count <= 0) {
            refreshHandheldCaptureUiState();
            return;
        }
        refreshHandheldCaptureUiState();
    }
    private void handleHandheldFinishAction() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        if (stopBtn != null && !stopBtn.isEnabled()) {
            refreshHandheldCaptureUiState();
            return;
        }
        if (!finishHandheldBoundaryCapture()) {
            refreshHandheldCaptureUiState();
        }
    }
    private void enterHandheldCaptureInlineUi() {
        if (handheldCaptureInlineUiActive) {
            refreshHandheldCaptureUiState();
            return;
        }
        handheldCaptureInlineUiActive = true;
        handheldCaptureStoredStatusText = statusLabel != null ? statusLabel.getText() : null;
        if (statusLabel != null) {
            statusLabel.setText("手持采集中");
        }
        if (startBtn != null) {
            handheldStartButtonOriginalBackground = startBtn.getBackground();
            handheldStartButtonOriginalForeground = startBtn.getForeground();
            startBtn.setIcon(null);
            startBtn.setIconTextGap(0);
            startBtn.setHorizontalAlignment(SwingConstants.CENTER);
            startBtn.setHorizontalTextPosition(SwingConstants.CENTER);
            startBtn.setVerticalTextPosition(SwingConstants.CENTER);
        }
        if (stopBtn != null) {
            handheldStopButtonOriginalBackground = stopBtn.getBackground();
            handheldStopButtonOriginalForeground = stopBtn.getForeground();
            stopBtn.setIcon(null);
            stopBtn.setIconTextGap(0);
            stopBtn.setHorizontalAlignment(SwingConstants.CENTER);
            stopBtn.setHorizontalTextPosition(SwingConstants.CENTER);
            stopBtn.setVerticalTextPosition(SwingConstants.CENTER);
            stopBtn.setText("结束");
        }
        startHandheldCaptureStatusTimer();
        refreshHandheldCaptureUiState();
    }
    private void exitHandheldCaptureInlineUi() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        handheldCaptureInlineUiActive = false;
        stopHandheldCaptureStatusTimer();
        if (statusLabel != null) {
            statusLabel.setText(handheldCaptureStoredStatusText != null ? handheldCaptureStoredStatusText : "待机");
        }
        if (startBtn != null) {
            startBtn.setToolTipText(null);
            if (handheldStartButtonOriginalBackground != null) {
                startBtn.setBackground(handheldStartButtonOriginalBackground);
            }
            if (handheldStartButtonOriginalForeground != null) {
                startBtn.setForeground(handheldStartButtonOriginalForeground);
            }
            startBtn.setEnabled(true);
            updateStartButtonAppearance();
        }
        if (stopBtn != null) {
            stopBtn.setToolTipText(null);
            if (handheldStopButtonOriginalBackground != null) {
                stopBtn.setBackground(handheldStopButtonOriginalBackground);
            }
            if (handheldStopButtonOriginalForeground != null) {
                stopBtn.setForeground(handheldStopButtonOriginalForeground);
            }
            stopBtn.setEnabled(true);
            stopBtn.setText("结束");
            updateStopButtonIcon();
        }
        handheldCaptureStoredStatusText = null;
        handheldStartButtonOriginalBackground = null;
        handheldStartButtonOriginalForeground = null;
        handheldStopButtonOriginalBackground = null;
        handheldStopButtonOriginalForeground = null;
    }
    private void startHandheldCaptureStatusTimer() {
        if (handheldCaptureStatusTimer == null) {
            handheldCaptureStatusTimer = new Timer(400, e -> refreshHandheldCaptureUiState());
            handheldCaptureStatusTimer.setRepeats(true);
        }
        if (!handheldCaptureStatusTimer.isRunning()) {
            handheldCaptureStatusTimer.start();
        }
    }
    private void stopHandheldCaptureStatusTimer() {
        if (handheldCaptureStatusTimer != null && handheldCaptureStatusTimer.isRunning()) {
            handheldCaptureStatusTimer.stop();
        }
    }
    // Update inline handheld capture buttons based on the current device reading.
    private void refreshHandheldCaptureUiState() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        int nextIndex = handheldCapturedPoints + 1;
        boolean hasFix = hasHighPrecisionFix();
        boolean hasValid = hasValidRealtimeHandheldPosition();
        boolean duplicate = hasValid && isCurrentHandheldPointDuplicate();
        boolean canConfirm = handheldCaptureActive && hasFix && hasValid && !duplicate;
        if (startBtn != null) {
            String prompt = "<html><center>采集点" + nextIndex + "<br>确定</center></html>";
            startBtn.setText(prompt);
            startBtn.setEnabled(canConfirm);
            if (canConfirm) {
                if (handheldStartButtonOriginalBackground != null) {
                    startBtn.setBackground(handheldStartButtonOriginalBackground);
                }
                if (handheldStartButtonOriginalForeground != null) {
                    startBtn.setForeground(handheldStartButtonOriginalForeground);
                }
                startBtn.setToolTipText(null);
            } else {
                startBtn.setBackground(new Color(200, 200, 200));
                startBtn.setForeground(new Color(130, 130, 130));
                startBtn.setToolTipText(resolveHandheldConfirmTooltip(hasFix, hasValid, duplicate));
            }
        }
        if (stopBtn != null) {
            boolean canFinish = handheldCapturedPoints >= 3;
            stopBtn.setText("结束");
            stopBtn.setEnabled(canFinish);
            if (canFinish) {
                if (handheldStopButtonOriginalBackground != null) {
                    stopBtn.setBackground(handheldStopButtonOriginalBackground);
                }
                if (handheldStopButtonOriginalForeground != null) {
                    stopBtn.setForeground(handheldStopButtonOriginalForeground);
                }
                stopBtn.setToolTipText("结束采集并返回新增地块");
            } else {
                stopBtn.setBackground(new Color(220, 220, 220));
                stopBtn.setForeground(new Color(130, 130, 130));
                stopBtn.setToolTipText("至少采集三个点才能结束");
            }
        }
    }
    private String resolveHandheldConfirmTooltip(boolean hasFix, boolean hasValidPosition, boolean duplicate) {
        if (!hasFix) {
            return "当前定位质量不足,无法采集";
        }
        if (!hasValidPosition) {
            return "当前定位数据无效,请稍后再试";
        }
        if (duplicate) {
            return "当前坐标已采集,请移动到新的位置";
        }
        return null;
    }
    private boolean hasHighPrecisionFix() {
        Device device = Device.getGecaoji();
        if (device == null) {
            return false;
        }
        String status = device.getPositioningStatus();
        return status != null && "4".equals(status.trim());
    }
    private boolean canConfirmHandheldPoint() {
        return handheldCaptureActive
            && hasHighPrecisionFix()
            && hasValidRealtimeHandheldPosition()
            && !isCurrentHandheldPointDuplicate();
    }
    private void enterDrawingControlMode() {
        if (drawingControlModeActive) {
            return;
        }
        storedStartButtonShowingPause = startButtonShowingPause;
        storedStopButtonActive = stopButtonActive;
        storedStatusBeforeDrawing = statusLabel != null ? statusLabel.getText() : null;
        drawingControlModeActive = true;
        applyDrawingPauseState(false, false);
        updateDrawingControlButtonLabels();
    }
    private void exitDrawingControlMode() {
        if (!drawingControlModeActive) {
            return;
        }
        drawingControlModeActive = false;
        applyDrawingPauseState(false, false);
        drawingPaused = false;
        stopButtonActive = storedStopButtonActive;
        startButtonShowingPause = storedStartButtonShowingPause;
        if (startBtn != null) {
            updateStartButtonAppearance();
        }
        if (stopBtn != null) {
            stopBtn.setText("结束");
            updateStopButtonIcon();
        }
        if (statusLabel != null) {
            statusLabel.setText(storedStatusBeforeDrawing != null ? storedStatusBeforeDrawing : "待机");
        }
        storedStatusBeforeDrawing = null;
    }
    private void updateDrawingControlButtonLabels() {
        if (!drawingControlModeActive) {
            return;
        }
        configureButtonForDrawingMode(startBtn);
        configureButtonForDrawingMode(stopBtn);
        if (startBtn != null) {
            startBtn.setText(drawingPaused ? "开始绘制" : "暂停绘制");
        }
        if (stopBtn != null) {
            stopBtn.setText("结束绘制");
        }
    }
    private void configureButtonForDrawingMode(JButton button) {
        if (button == null) {
            return;
        }
        button.setIcon(null);
        button.setIconTextGap(0);
        button.setHorizontalAlignment(SwingConstants.CENTER);
        button.setHorizontalTextPosition(SwingConstants.CENTER);
    }
    private void updateStartButtonAppearance() {
        if (startBtn == null) {
            return;
@@ -1028,6 +1484,13 @@
        refreshMowerSpeedLabel();
    }
    public void setHandheldMowerIconActive(boolean active) {
        if (mapRenderer == null) {
            return;
        }
        mapRenderer.setHandheldMowerIconActive(active);
    }
    public boolean startMowerBoundaryCapture() {
        if (mapRenderer == null) {
            return false;
@@ -1037,6 +1500,8 @@
            return false;
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.MOWER;
        mowerBoundaryCaptureActive = true;
        mowerBaseLatLon = baseLatLonCandidate;
@@ -1053,9 +1518,12 @@
        Coordinate.setStartSaveGngga(true);
        if (mapRenderer != null) {
            mapRenderer.setBoundaryPreviewMarkerScale(2.0d);
            mapRenderer.beginHandheldBoundaryPreview();
        }
        setHandheldMowerIconActive(false);
        startMowerBoundaryMonitor();
        return true;
    }
@@ -1064,15 +1532,12 @@
        if (mapRenderer == null) {
            return false;
        }
        if (handheldCaptureDialog != null && handheldCaptureDialog.isShowing()) {
            handheldCaptureDialog.toFront();
            return true;
        }
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.HANDHELD;
        handheldCaptureActive = true;
        handheldCapturedPoints = 0;
@@ -1084,19 +1549,10 @@
            handheldTemporaryPoints.clear();
        }
        AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList());
        mapRenderer.beginHandheldBoundaryPreview();
        Window ownerWindow = SwingUtilities.getWindowAncestor(this);
        SwingUtilities.invokeLater(() -> {
            Window targetOwner = ownerWindow;
            if (targetOwner == null) {
                targetOwner = SwingUtilities.getWindowAncestor(Shouye.this);
            }
            HandheldBoundaryCaptureDialog dialog = new HandheldBoundaryCaptureDialog(targetOwner, Shouye.this, visualizationPanel, THEME_COLOR);
            handheldCaptureDialog = dialog;
            dialog.setVisible(true);
        });
    mapRenderer.setBoundaryPreviewMarkerScale(1.0d);
    mapRenderer.beginHandheldBoundaryPreview();
        setHandheldMowerIconActive(true);
        enterHandheldCaptureInlineUi();
        return true;
    }
@@ -1188,6 +1644,7 @@
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
        setHandheldMowerIconActive(false);
    }
    private void discardLatestCoordinate(Coordinate coordinate) {
@@ -1282,6 +1739,7 @@
        List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot();
        handheldCaptureActive = false;
        activeBoundaryMode = BoundaryCaptureMode.NONE;
        Coordinate.setStartSaveGngga(false);
        if (mapRenderer != null) {
            mapRenderer.clearHandheldBoundaryPreview();
@@ -1289,20 +1747,12 @@
    AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot);
        exitHandheldCaptureInlineUi();
        SwingUtilities.invokeLater(AddDikuai::finishDrawingSession);
        return true;
    }
    void handheldBoundaryCaptureDialogClosed(HandheldBoundaryCaptureDialog dialog) {
        if (handheldCaptureDialog == dialog) {
            handheldCaptureDialog = null;
        }
        handheldCaptureActive = false;
        if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
    }
    int getHandheldCapturedPointCount() {
        return handheldCapturedPoints;
    }
@@ -1571,9 +2021,9 @@
        if (statusLabel == null) {
            return;
        }
        if ("作业中".equals(statusText)) {
        if ("作业中".equals(statusText) || "绘制中".equals(statusText)) {
            statusLabel.setForeground(THEME_COLOR);
        } else if ("暂停中".equals(statusText)) {
        } else if ("暂停中".equals(statusText) || "绘制暂停".equals(statusText)) {
            statusLabel.setForeground(STATUS_PAUSE_COLOR);
        } else {
            statusLabel.setForeground(Color.GRAY);
@@ -1601,6 +2051,29 @@
        return button;
    }
    private JButton createFloatingTextButton(String text) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.BOLD, 15));
        button.setForeground(Color.WHITE);
        button.setBackground(THEME_COLOR);
        button.setBorder(BorderFactory.createEmptyBorder(10, 18, 10, 18));
        button.setFocusPainted(false);
        button.setOpaque(true);
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setBackground(THEME_HOVER_COLOR);
            }
            @Override
            public void mouseExited(MouseEvent e) {
                button.setBackground(THEME_COLOR);
            }
        });
        return button;
    }
    private ImageIcon loadScaledIcon(String path, int width, int height) {
        try {
            ImageIcon icon = new ImageIcon(path);
@@ -1658,6 +2131,12 @@
        if (notifyCoordinate) {
            Coordinate.setStartSaveGngga(!paused);
        }
        if (drawingControlModeActive) {
            updateDrawingControlButtonLabels();
            if (statusLabel != null) {
                statusLabel.setText(paused ? "绘制暂停" : "绘制中");
            }
        }
    }
    private void toggleDrawingPause() {
@@ -1670,36 +2149,33 @@
    public void showEndDrawingButton(Runnable callback, String drawingShape) {
        endDrawingCallback = callback;
        applyDrawingPauseState(false, false);
        circleDialogMode = false;
        hideCircleGuidancePanel();
        ensureFloatingIconsLoaded();
        ensureFloatingButtonInfrastructure();
        enterDrawingControlMode();
        boolean enableCircleGuidance = drawingShape != null
                && "circle".equalsIgnoreCase(drawingShape.trim());
        if (enableCircleGuidance) {
            prepareCircleGuidanceState();
            showCircleGuidanceStep(1);
            endDrawingButton.setVisible(false);
            ensureFloatingIconsLoaded();
            ensureFloatingButtonInfrastructure();
            if (drawingPauseButton != null) {
                drawingPauseButton.setVisible(false);
            }
            if (endDrawingButton != null) {
                endDrawingButton.setVisible(false);
            }
            prepareCircleGuidanceState();
            showCircleGuidanceStep(1);
            floatingButtonPanel.setVisible(true);
            if (floatingButtonPanel.getParent() != visualizationPanel) {
                visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
            }
            rebuildFloatingButtonColumn();
        } else {
            clearCircleGuidanceArtifacts();
            endDrawingButton.setVisible(true);
            if (drawingPauseButton != null) {
                drawingPauseButton.setVisible(true);
            }
            hideFloatingDrawingControls();
        }
        floatingButtonPanel.setVisible(true);
        if (floatingButtonPanel.getParent() != visualizationPanel) {
            visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
        }
        rebuildFloatingButtonColumn();
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
@@ -1776,6 +2252,14 @@
                floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10)));
            }
            floatingButtonColumn.add(endDrawingButton);
            added = true;
        }
        if (pathPreviewReturnButton != null && pathPreviewReturnButton.isVisible()) {
            if (added) {
                floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10)));
            }
            floatingButtonColumn.add(pathPreviewReturnButton);
            added = true;
        }
        floatingButtonColumn.revalidate();
        floatingButtonColumn.repaint();
@@ -2184,21 +2668,27 @@
    private double[] resolveCircleBaseLatLon() {
        String coords = null;
        String landNumber = Dikuaiguanli.getCurrentWorkLandNumber();
        if (isMeaningfulValue(landNumber)) {
            Dikuai current = Dikuai.getDikuai(landNumber);
            if (current != null) {
                coords = current.getBaseStationCoordinates();
        if (baseStation == null) {
            baseStation = new BaseStation();
        }
        baseStation.load();
        coords = baseStation.getInstallationCoordinates();
        if (!isMeaningfulValue(coords)) {
            String landNumber = Dikuaiguanli.getCurrentWorkLandNumber();
            if (isMeaningfulValue(landNumber)) {
                Dikuai current = Dikuai.getDikuai(landNumber);
                if (current != null) {
                    coords = current.getBaseStationCoordinates();
                }
            }
        }
        if (!isMeaningfulValue(coords)) {
            coords = addzhangaiwu.getActiveSessionBaseStation();
        }
        if (!isMeaningfulValue(coords) && baseStation != null) {
            coords = baseStation.getInstallationCoordinates();
        }
        if (!isMeaningfulValue(coords)) {
            return null;
            if (!isMeaningfulValue(coords)) {
                coords = addzhangaiwu.getActiveSessionBaseStation();
            }
            if (!isMeaningfulValue(coords)) {
                return null;
            }
        }
        String[] parts = coords.split(",");
        if (parts.length < 4) {
@@ -2330,7 +2820,9 @@
        clearCircleGuidanceArtifacts();
        hideFloatingDrawingControls();
        circleDialogMode = false;
        applyDrawingPauseState(false, false);
        exitHandheldCaptureInlineUi();
        handheldCaptureActive = false;
        exitDrawingControlMode();
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) {
@@ -2339,6 +2831,129 @@
        endDrawingCallback = null;
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
        setHandheldMowerIconActive(false);
    }
    private void showPathPreviewReturnControls() {
        ensureFloatingButtonInfrastructure();
        if (drawingPauseButton != null) {
            drawingPauseButton.setVisible(false);
        }
        if (endDrawingButton != null) {
            endDrawingButton.setVisible(false);
        }
        if (pathPreviewReturnButton == null) {
            pathPreviewReturnButton = createFloatingTextButton("返回");
            pathPreviewReturnButton.setToolTipText("返回新增地块步骤");
            pathPreviewReturnButton.addActionListener(e -> handlePathPreviewReturn());
        }
        pathPreviewReturnButton.setVisible(true);
        if (floatingButtonPanel != null) {
            floatingButtonPanel.setVisible(true);
            if (floatingButtonPanel.getParent() != visualizationPanel) {
                visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
            }
        }
        rebuildFloatingButtonColumn();
    }
    private void hidePathPreviewReturnControls() {
        if (pathPreviewReturnButton != null) {
            pathPreviewReturnButton.setVisible(false);
        }
        rebuildFloatingButtonColumn();
        if (floatingButtonPanel != null && floatingButtonColumn != null
                && floatingButtonColumn.getComponentCount() == 0) {
            floatingButtonPanel.setVisible(false);
        }
    }
    private void handlePathPreviewReturn() {
        Runnable callback = pathPreviewReturnAction;
        exitMowingPathPreview();
        if (callback != null) {
            callback.run();
        }
    }
    public boolean startMowingPathPreview(String landNumber,
                                          String landName,
                                          String boundary,
                                          String obstacles,
                                          String plannedPath,
                                          Runnable returnAction) {
        if (mapRenderer == null || !isMeaningfulValue(plannedPath)) {
            return false;
        }
        if (pathPreviewActive) {
            exitMowingPathPreview();
        }
        exitDrawingControlMode();
        hideCircleGuidancePanel();
        clearCircleGuidanceArtifacts();
        pathPreviewReturnAction = returnAction;
        pathPreviewActive = true;
    mapRenderer.setPathPreviewSizingEnabled(true);
        previewRestoreLandNumber = Dikuaiguanli.getCurrentWorkLandNumber();
        previewRestoreLandName = null;
        if (isMeaningfulValue(previewRestoreLandNumber)) {
            Dikuai existing = Dikuai.getDikuai(previewRestoreLandNumber);
            if (existing != null) {
                previewRestoreLandName = existing.getLandName();
            }
        }
        mapRenderer.setCurrentBoundary(boundary, landNumber, landName);
        mapRenderer.setCurrentObstacles(obstacles, landNumber);
        mapRenderer.setCurrentPlannedPath(plannedPath);
        mapRenderer.clearHandheldBoundaryPreview();
    mapRenderer.setBoundaryPointSizeScale(1.0d);
        mapRenderer.setBoundaryPointsVisible(isMeaningfulValue(boundary));
        String displayName = isMeaningfulValue(landName) ? landName : landNumber;
        updateCurrentAreaName(displayName);
        showPathPreviewReturnControls();
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
        return true;
    }
    public void exitMowingPathPreview() {
        if (!pathPreviewActive) {
            return;
        }
        pathPreviewActive = false;
        if (mapRenderer != null) {
            mapRenderer.setPathPreviewSizingEnabled(false);
        }
        hidePathPreviewReturnControls();
        String restoreNumber = previewRestoreLandNumber;
        String restoreName = previewRestoreLandName;
        previewRestoreLandNumber = null;
        previewRestoreLandName = null;
        pathPreviewReturnAction = null;
        if (restoreNumber != null) {
            Dikuaiguanli.setCurrentWorkLand(restoreNumber, restoreName);
        } else if (mapRenderer != null) {
            mapRenderer.setCurrentBoundary(null, null, null);
            mapRenderer.setCurrentObstacles((String) null, null);
            mapRenderer.setCurrentPlannedPath(null);
            mapRenderer.setBoundaryPointsVisible(false);
            mapRenderer.setBoundaryPointSizeScale(1.0d);
            mapRenderer.clearHandheldBoundaryPreview();
            mapRenderer.resetView();
            updateCurrentAreaName(null);
        }
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
    
    /**
@@ -2370,6 +2985,16 @@
    private void initializeDefaultAreaSelection() {
        Dikuai.initFromProperties();
        String persistedLandNumber = Dikuaiguanli.getPersistedWorkLandNumber();
        if (persistedLandNumber != null) {
            Dikuai stored = Dikuai.getDikuai(persistedLandNumber);
            if (stored != null) {
                Dikuaiguanli.setCurrentWorkLand(persistedLandNumber, stored.getLandName());
                return;
            }
            Dikuaiguanli.setCurrentWorkLand(null, null);
        }
        Map<String, Dikuai> all = Dikuai.getAllDikuai();
        if (all.isEmpty()) {
            Dikuaiguanli.setCurrentWorkLand(null, null);
src/zhuye/adddikuaiyulan.java
@@ -17,6 +17,10 @@
    private static final Color HANDHELD_BOUNDARY_FILL = new Color(51, 153, 255, 60);
    private static final Color HANDHELD_BOUNDARY_BORDER = new Color(0, 100, 0, 220);
    private static final Color HANDHELD_BOUNDARY_POINT = new Color(0, 100, 0);
    private static final double BASE_WORLD_MARKER_SIZE = 0.27d; // halve the base diameter for subtler markers
    private static final double MIN_PIXEL_DIAMETER = 3.0d;
    private static final double MAX_PIXEL_DIAMETER = 9.0d;
    private static volatile double cachedMarkerPixelDiameter = -1.0d;
    private adddikuaiyulan() {
    }
@@ -24,7 +28,12 @@
    public static void drawPreview(Graphics2D g2d,
                                   List<Point2D.Double> previewPoints,
                                   double scale,
                                   boolean previewActive) {
                                   boolean previewActive,
                                   double diameterScale) {
        if (!previewActive) {
            cachedMarkerPixelDiameter = -1.0d;
        }
        if (g2d == null || !previewActive || previewPoints == null || previewPoints.isEmpty()) {
            return;
        }
@@ -56,13 +65,25 @@
            g2d.fill(path);
        }
        float outlineWidth =0.1f;
        float outlineWidth = 0.1f;
        g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g2d.setColor(HANDHELD_BOUNDARY_BORDER);
        g2d.draw(path);
        double markerSize = 0.2d;
        double markerRadius = markerSize / 2.0d;
        if (cachedMarkerPixelDiameter <= 0.0d) {
            double previousPixelDiameter = Math.abs(BASE_WORLD_MARKER_SIZE * scale);
            if (previousPixelDiameter <= 0.0d) {
                previousPixelDiameter = MIN_PIXEL_DIAMETER;
            }
            cachedMarkerPixelDiameter = Math.max(MIN_PIXEL_DIAMETER,
                Math.min(MAX_PIXEL_DIAMETER, previousPixelDiameter));
        }
    double effectiveScale = Math.max(0.01d, scale);
    double markerSize = cachedMarkerPixelDiameter / effectiveScale;
    double normalizedScale = Double.isFinite(diameterScale) && diameterScale > 0.0d ? diameterScale : 1.0d;
    markerSize *= normalizedScale;
    double markerRadius = markerSize / 2.0d;
        for (Point2D.Double point : previewPoints) {
            if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) {
src/zhuye/pointandnumber.java
@@ -20,7 +20,8 @@
                                          List<Point2D.Double> boundary, // è¾¹ç•Œç‚¹é›†åˆ
                                          double scale, // ç¼©æ”¾æ¯”例
                                          double mergeThreshold, // åˆå¹¶é˜ˆå€¼
                                          Color pointColor) { // ç‚¹é¢œè‰²
                                          Color pointColor, // ç‚¹é¢œè‰²
                                          double diameterScale) { // ç›´å¾„缩放因子
        if (boundary == null || boundary.size() < 2) { // åˆ¤æ–­æ•°æ®æ˜¯å¦æœ‰æ•ˆ
            return; // æ— æ•ˆç›´æŽ¥è¿”回
        }
@@ -32,8 +33,10 @@
            return; // æ— æ•ˆè¿”回
        }
        double scaleFactor = Math.max(0.5, scale); // é˜²æ­¢è¿‡å°ç¼©æ”¾
        double markerDiameter = Math.max(1.0, (10.0 / scaleFactor) * 0.2); // æç‚¹ç›´å¾„
    double scaleFactor = Math.max(0.5, scale); // é˜²æ­¢è¿‡å°ç¼©æ”¾
    double clampedScale = diameterScale > 0 ? diameterScale : 1.0; // é˜²æ­¢éžæ³•缩放
    double minimumDiameter = clampedScale < 1.0 ? 0.5 : 1.0; // ç¼©å°æ—¶å…è®¸æ›´å°çš„æœ€å°å€¼
    double markerDiameter = Math.max(minimumDiameter, (10.0 / scaleFactor) * 0.2 * clampedScale); // æç‚¹ç›´å¾„
        double markerRadius = markerDiameter / 2.0; // åŠå¾„
        for (int i = 0; i < effectiveCount; i++) { // éåŽ†æœ‰æ•ˆç‚¹
src/zhuye/zijian.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package zhuye;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.Window;
/**
 * è‡ªæ£€æç¤ºå·¥å…·ç±»ï¼Œé›†ä¸­ç®¡ç†å‰²è‰æœºä½œä¸šå‰çš„自检提示逻辑。
 */
public final class zijian {
    private static boolean selfCheckCompleted;
    private static boolean initialPromptShown;
    private zijian() {
        // Utility class
    }
    public static boolean ensureBeforeMowing(Component anchorComponent, Runnable onSelfCheckConfirmed) {
        if (selfCheckCompleted) {
            return true;
        }
        showSelfCheckDialog(anchorComponent, onSelfCheckConfirmed);
        return false;
    }
    public static void showInitialPromptIfNeeded(Component anchorComponent, Runnable onSelfCheckConfirmed) {
        if (selfCheckCompleted || initialPromptShown) {
            return;
        }
        initialPromptShown = true;
        showSelfCheckDialog(anchorComponent, onSelfCheckConfirmed);
    }
    public static void markSelfCheckCompleted() {
        selfCheckCompleted = true;
    }
    private static void showSelfCheckDialog(Component anchorComponent, Runnable onSelfCheckConfirmed) {
    Component parent = resolveDialogParent(anchorComponent);
    Object[] options = {"立即自检", "取消"};
    String message = "<html>割草前请先完成对割草机的自检操作。<br>遥控割草机向前开约2米,然后原地转圈完成自检功能。</html>";
    JOptionPane optionPane = new JOptionPane(
        message,
        JOptionPane.INFORMATION_MESSAGE,
        JOptionPane.DEFAULT_OPTION,
        null,
        options,
        options[0]);
    JDialog dialog = optionPane.createDialog(parent, "自检提示");
    dialog.pack();
    applyTargetWidth(anchorComponent, dialog);
    dialog.setLocationRelativeTo(parent instanceof Component ? (Component) parent : null);
    dialog.setVisible(true);
        Object selectedValue = optionPane.getValue();
        dialog.dispose();
        boolean confirmed = options[0].equals(selectedValue);
        if (confirmed) {
            selfCheckCompleted = true;
            if (onSelfCheckConfirmed != null) {
                SwingUtilities.invokeLater(onSelfCheckConfirmed);
            }
        }
    }
    private static void applyTargetWidth(Component anchorComponent, JDialog dialog) {
        if (dialog == null) {
            return;
        }
        int targetWidth = resolveTargetDialogWidth(anchorComponent);
        if (targetWidth <= 0) {
            return;
        }
        Dimension currentSize = dialog.getSize();
        if (currentSize == null) {
            currentSize = dialog.getPreferredSize();
        }
        if (currentSize == null) {
            currentSize = new Dimension(targetWidth, 0);
        }
        int width = Math.max(targetWidth, currentSize.width);
        int height = currentSize.height > 0 ? currentSize.height : dialog.getHeight();
        if (height <= 0) {
            height = 200;
        }
        dialog.setSize(new Dimension(width, height));
    }
    private static int resolveTargetDialogWidth(Component anchorComponent) {
        int baseWidth = 0;
        if (anchorComponent != null) {
            baseWidth = anchorComponent.getWidth();
            if (baseWidth <= 0) {
                Component parent = resolveDialogParent(anchorComponent);
                if (parent != null) {
                    baseWidth = parent.getWidth();
                }
            }
        }
        if (baseWidth <= 0) {
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            baseWidth = screenSize != null ? screenSize.width : 0;
        }
        if (baseWidth <= 0) {
            return 0;
        }
        return (int) Math.round(baseWidth * 0.8);
    }
    private static Component resolveDialogParent(Component anchorComponent) {
        if (anchorComponent == null) {
            return null;
        }
        if (anchorComponent instanceof Window) {
            return anchorComponent;
        }
        Window window = SwingUtilities.getWindowAncestor(anchorComponent);
        return window != null ? window : anchorComponent;
    }
}
user.properties
@@ -1,10 +1,10 @@
#Updated User Properties
#Sun Nov 23 11:24:29 GMT+08:00 2025
registrationTime=-1
#Tue Dec 09 17:21:03 CST 2025
email=789
language=zh
lastLoginTime=-1
password=123
language=zh
userName=233
userId=-1
email=789
registrationTime=-1
status=-1
userId=-1
userName=233