826220679@qq.com
2 天以前 64e0880d2d81ce2b3f0e366b1537c5efe2f2c4ea
需改优化了绘制往返路径
已添加4个文件
已修改4个文件
819 ■■■■■ 文件已修改
image/backbutton.png 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Huizhiwanfanpath.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/SavaXyZuobiao.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/WangfanpathJisuan.java 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/publicway/Fanhuibutton.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/WangfanDraw.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
image/backbutton.png
set.properties
@@ -1,5 +1,5 @@
#Mower Configuration Properties - Updated
#Sun Dec 21 12:36:00 GMT+08:00 2025
#Sun Dec 21 20:48:14 GMT+08:00 2025
appVersion=-1
simCardNumber=-1
currentWorkLandNumber=LAND2
@@ -7,13 +7,13 @@
boundaryLengthVisible=false
idleTrailDurationSeconds=60
handheldMarkerId=1872
viewCenterX=-13.31
viewCenterY=3.75
viewCenterX=-37.40
viewCenterY=0.91
manualBoundaryDrawingMode=false
mowerId=860
serialPortName=COM15
serialAutoConnect=true
mapScale=11.63
mapScale=11.61
measurementModeEnabled=false
firmwareVersion=-1
cuttingWidth=200
src/dikuai/Huizhiwanfanpath.java
@@ -557,8 +557,9 @@
                });
            };
            
            // å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
            if (!shouye.startReturnPathDrawing(finishCallback)) {
            // å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶ï¼Œä¼ é€’æ˜¯å¦ä¸ºæ‰‹æŒè®¾å¤‡æ¨¡å¼
            boolean isHandheld = "handheld".equals(method);
            if (!shouye.startReturnPathDrawing(finishCallback, isHandheld)) {
                JOptionPane.showMessageDialog(this, "未能开始绘制,请确认设备状态和基准站设置后重试", "提示", JOptionPane.WARNING_MESSAGE);
                return false;
            }
src/lujing/SavaXyZuobiao.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
package lujing;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * ä¿å­˜X,Y坐标的工具类
 * åæ ‡å•位:米,精确到小数点后2位小数
 */
public class SavaXyZuobiao {
    // åæ ‡é›†åˆï¼Œä½¿ç”¨çº¿ç¨‹å®‰å…¨çš„List
    private static final List<double[]> COORDINATES = Collections.synchronizedList(new ArrayList<>());
    // ä¿å­˜çŠ¶æ€æ ‡å¿—
    private static volatile boolean isSaving = false;
    // æ•°å­—格式化器,用于保留2位小数
    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.00");
    // ç§æœ‰æž„造方法,防止实例化
    private SavaXyZuobiao() {
        throw new UnsupportedOperationException("这是一个工具类,不能实例化");
    }
    /**
     * æ·»åŠ åæ ‡åˆ°é›†åˆä¸­ï¼ˆä»…åœ¨ä¿å­˜çŠ¶æ€ä¸‹ï¼‰
     * @param x X坐标(米)
     * @param y Y坐标(米)
     */
    public static void addCoordinate(double x, double y) {
        if (isSaving) {
            // æ ¼å¼åŒ–坐标,保留2位小数
            double formattedX = Double.parseDouble(DECIMAL_FORMAT.format(x));
            double formattedY = Double.parseDouble(DECIMAL_FORMAT.format(y));
            double[] coordinate = {formattedX, formattedY};
            synchronized (COORDINATES) {
                COORDINATES.add(coordinate);
            }
        }
    }
    /**
     * èŽ·å–åæ ‡é›†åˆçš„å­—ç¬¦ä¸²è¡¨ç¤º
     * æ ¼å¼ï¼šX1,Y1;X2,Y2;...;Xn,Yn
     * @return æ ¼å¼åŒ–后的坐标字符串
     */
    public static String getCoordinatesString() {
        if (COORDINATES.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        synchronized (COORDINATES) {
            for (int i = 0; i < COORDINATES.size(); i++) {
                double[] coordinate = COORDINATES.get(i);
                sb.append(DECIMAL_FORMAT.format(coordinate[0]))
                  .append(",")
                  .append(DECIMAL_FORMAT.format(coordinate[1]));
                if (i < COORDINATES.size() - 1) {
                    sb.append(";");
                }
            }
        }
        return sb.toString();
    }
    /**
     * å¼€å§‹ä¿å­˜åæ ‡
     */
    public static void startSaving() {
        isSaving = true;
    }
    /**
     * æš‚停保存坐标
     */
    public static void pauseSaving() {
        isSaving = false;
    }
    /**
     * èŽ·å–å½“å‰ä¿å­˜çŠ¶æ€
     * @return true表示正在保存,false表示已暂停
     */
    public static boolean isSaving() {
        return isSaving;
    }
    /**
     * æ¸…空坐标集合中的所有数据
     */
    public static void clearCoordinates() {
        synchronized (COORDINATES) {
            COORDINATES.clear();
        }
    }
    /**
     * èŽ·å–å½“å‰ä¿å­˜çš„åæ ‡æ•°é‡
     * @return åæ ‡æ•°é‡
     */
    public static int getCoordinateCount() {
        synchronized (COORDINATES) {
            return COORDINATES.size();
        }
    }
    /**
     * èŽ·å–æ‰€æœ‰åæ ‡çš„åˆ—è¡¨ï¼ˆå‰¯æœ¬ï¼Œé˜²æ­¢å¤–éƒ¨ä¿®æ”¹å†…éƒ¨æ•°æ®ï¼‰
     * @return åæ ‡åˆ—表的副本
     */
    public static List<double[]> getAllCoordinates() {
        synchronized (COORDINATES) {
            List<double[]> copy = new ArrayList<>(COORDINATES.size());
            for (double[] coord : COORDINATES) {
                copy.add(new double[]{coord[0], coord[1]});
            }
            return copy;
        }
    }
}
src/lujing/WangfanpathJisuan.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,531 @@
package lujing;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
public class WangfanpathJisuan {
    /**
     * åæ ‡ç‚¹ç±»ï¼Œä½¿ç”¨è®°å½•处理提高精度控制
     */
    private static class Point {
        final double x;
        final double y;
        final int originalIndex; // è®°å½•原始位置,便于调试
        Point(double x, double y, int index) {
            this.x = x;
            this.y = y;
            this.originalIndex = index;
        }
        Point(double x, double y) {
            this(x, y, -1);
        }
        double distanceTo(Point other) {
            if (other == null) return Double.MAX_VALUE;
            double dx = this.x - other.x;
            double dy = this.y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Point point = (Point) obj;
            // ä½¿ç”¨1e-6的精度判断相等,比直接比较double更稳定
            return Math.abs(point.x - x) < 1e-6 && Math.abs(point.y - y) < 1e-6;
        }
        @Override
        public int hashCode() {
            // ä½¿ç”¨å›ºå®šç²¾åº¦è¿›è¡Œå“ˆå¸Œè®¡ç®—,确保精度范围内相等的点有相同哈希值
            long xBits = Double.doubleToLongBits(Math.round(x * 1e6) / 1e6);
            long yBits = Double.doubleToLongBits(Math.round(y * 1e6) / 1e6);
            return (int)(xBits ^ (xBits >>> 32) ^ yBits ^ (yBits >>> 32));
        }
        @Override
        public String toString() {
            return String.format("%.3f,%.3f", x, y);
        }
        public String toString(int precision) {
            return String.format("%." + precision + "f,%." + precision + "f", x, y);
        }
    }
    /**
     * çº¿æ®µç±»ï¼Œç”¨äºŽè®¡ç®—点到线段的距离
     */
    private static class LineSegment {
        final Point start;
        final Point end;
        final double length;
        LineSegment(Point start, Point end) {
            this.start = start;
            this.end = end;
            this.length = start.distanceTo(end);
        }
        /**
         * è®¡ç®—点到线段的垂直距离
         */
        double perpendicularDistance(Point point) {
            if (length == 0) {
                return point.distanceTo(start);
            }
            // ä½¿ç”¨å‘量方法计算投影距离
            double x1 = start.x, y1 = start.y;
            double x2 = end.x, y2 = end.y;
            double x0 = point.x, y0 = point.y;
            // è®¡ç®—点到直线距离公式
            double numerator = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1);
            double denominator = Math.sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1));
            return numerator / denominator;
        }
        /**
         * åˆ¤æ–­ç‚¹æ˜¯å¦åœ¨çº¿æ®µçš„边界框内(用于判断是否在线段上)
         */
        boolean isPointInBoundingBox(Point point) {
            double minX = Math.min(start.x, end.x);
            double maxX = Math.max(start.x, end.x);
            double minY = Math.min(start.y, end.y);
            double maxY = Math.max(start.y, end.y);
            return point.x >= minX && point.x <= maxX &&
                   point.y >= minY && point.y <= maxY;
        }
    }
    /**
     * ä¼˜åŒ–配置类
     */
    public static class OptimizationConfig {
        private double distanceTolerance = 0.1;       // è·ç¦»å®¹å·®ï¼ˆç±³ï¼‰
        private double angleTolerance = 1.0;          // è§’度容差(度)
        private int outputPrecision = 3;              // è¾“出精度(小数位数)
        private boolean keepEndpoints = true;         // æ˜¯å¦ä¿ç•™ç«¯ç‚¹
        private boolean useFastSimplify = false;      // æ˜¯å¦ä½¿ç”¨å¿«é€Ÿç®€åŒ–算法
        public OptimizationConfig() {}
        public OptimizationConfig setDistanceTolerance(double tolerance) {
            this.distanceTolerance = Math.max(0.01, tolerance); // æœ€å°0.01ç±³
            return this;
        }
        public OptimizationConfig setAngleTolerance(double degrees) {
            this.angleTolerance = Math.max(0.1, Math.min(degrees, 45)); // é™åˆ¶åœ¨0.1-45度
            return this;
        }
        public OptimizationConfig setOutputPrecision(int precision) {
            this.outputPrecision = Math.max(0, Math.min(precision, 8)); // é™åˆ¶åœ¨0-8位
            return this;
        }
        public OptimizationConfig setKeepEndpoints(boolean keep) {
            this.keepEndpoints = keep;
            return this;
        }
        public OptimizationConfig setUseFastSimplify(boolean useFast) {
            this.useFastSimplify = useFast;
            return this;
        }
    }
    private OptimizationConfig config;
    public WangfanpathJisuan() {
        this.config = new OptimizationConfig();
    }
    public WangfanpathJisuan(OptimizationConfig config) {
        this.config = config;
    }
    /**
     * ä¸»ä¼˜åŒ–方法
     */
    public String optimizePath(String pathStr) {
        return optimizePath(pathStr, this.config);
    }
    /**
     * å¸¦é…ç½®çš„优化方法
     */
    public String optimizePath(String pathStr, OptimizationConfig config) {
        if (pathStr == null || pathStr.trim().isEmpty()) {
            return "";
        }
        List<Point> points = parsePoints(pathStr);
        if (points.size() <= 2) {
            return pointsToString(points, config.outputPrecision);
        }
        // æ‰§è¡Œä¼˜åŒ–流水线
        List<Point> result = optimizationPipeline(points, config);
        return pointsToString(result, config.outputPrecision);
    }
    /**
     * ä¼˜åŒ–流水线:按顺序执行多个优化步骤
     */
    private List<Point> optimizationPipeline(List<Point> points, OptimizationConfig config) {
        List<Point> result = new ArrayList<>(points);
        // æ­¥éª¤1: åŽ»é™¤è¿žç»­é‡å¤ç‚¹
        result = removeConsecutiveDuplicates(result);
        // æ­¥éª¤2: æ ¹æ®é…ç½®é€‰æ‹©ç®€åŒ–算法
        if (config.useFastSimplify) {
            result = fastSimplify(result, config.distanceTolerance, config.angleTolerance);
        } else {
            result = douglasPeuckerSimplify(result, config.distanceTolerance);
        }
        // æ­¥éª¤3: ç¡®ä¿ç«¯ç‚¹ï¼ˆå¯é€‰ï¼‰
        if (config.keepEndpoints && result.size() > 1) {
            ensureEndpoints(points, result);
        }
        return result;
    }
    /**
     * è§£æžåæ ‡ç‚¹ï¼Œå¸¦ä½ç½®ç´¢å¼•
     */
    private List<Point> parsePoints(String pathStr) {
        List<Point> points = new ArrayList<>();
        String[] pointStrs = pathStr.split(";");
        for (int i = 0; i < pointStrs.length; i++) {
            String pointStr = pointStrs[i].trim();
            if (pointStr.isEmpty()) continue;
            String[] xy = pointStr.split(",");
            if (xy.length != 2) continue;
            try {
                double x = Double.parseDouble(xy[0].trim());
                double y = Double.parseDouble(xy[1].trim());
                points.add(new Point(x, y, i));
            } catch (NumberFormatException e) {
                // è·³è¿‡æ ¼å¼é”™è¯¯çš„点,记录日志(实际使用时可添加日志)
                continue;
            }
        }
        return points;
    }
    /**
     * åŽ»é™¤è¿žç»­é‡å¤ç‚¹ï¼ˆä¼˜åŒ–ç‰ˆï¼‰
     */
    private List<Point> removeConsecutiveDuplicates(List<Point> points) {
        if (points.size() <= 1) {
            return new ArrayList<>(points);
        }
        List<Point> result = new ArrayList<>(points.size());
        result.add(points.get(0));
        for (int i = 1; i < points.size(); i++) {
            Point current = points.get(i);
            Point last = result.get(result.size() - 1);
            // ä½¿ç”¨è·ç¦»åˆ¤æ–­æ˜¯å¦é‡å¤ï¼Œè€ƒè™‘浮点精度
            if (current.distanceTo(last) > config.distanceTolerance * 0.1) {
                result.add(current);
            }
            // å¦‚果距离很小但实际是不同的点(浮点误差),仍保留
            else if (!current.equals(last)) {
                result.add(current);
            }
        }
        return result;
    }
    /**
     * å¿«é€Ÿç®€åŒ–算法(结合距离和角度判断)
     */
    private List<Point> fastSimplify(List<Point> points, double distanceTolerance, double angleToleranceDeg) {
        if (points.size() < 3) {
            return new ArrayList<>(points);
        }
        List<Point> result = new ArrayList<>();
        result.add(points.get(0));
        int i = 1;
        while (i < points.size() - 1) {
            Point prev = result.get(result.size() - 1);
            Point current = points.get(i);
            Point next = points.get(i + 1);
            // æ£€æŸ¥è·ç¦»æ¡ä»¶
            double distToPrev = current.distanceTo(prev);
            double distToNext = current.distanceTo(next);
            // æ£€æŸ¥è§’度条件(将角度容差转换为弧度)
            double angleToleranceRad = Math.toRadians(angleToleranceDeg);
            double angle = calculateAngle(prev, current, next);
            // å¦‚果点距离前一点或后一点很近,或者三点形成的角度接近180度(共线),则剔除当前点
            if (distToPrev < distanceTolerance ||
                distToNext < distanceTolerance ||
                Math.abs(Math.PI - angle) < angleToleranceRad) {
                i++; // è·³è¿‡å½“前点
            } else {
                result.add(current);
                i++;
            }
        }
        // æ·»åŠ æœ€åŽä¸€ä¸ªç‚¹
        if (points.size() > 1) {
            result.add(points.get(points.size() - 1));
        }
        return result;
    }
    /**
     * è®¡ç®—三点形成的角度(以中间点为顶点)
     */
    private double calculateAngle(Point a, Point b, Point c) {
        double baX = a.x - b.x;
        double baY = a.y - b.y;
        double bcX = c.x - b.x;
        double bcY = c.y - b.y;
        double dotProduct = baX * bcX + baY * bcY;
        double magnitudeBA = Math.sqrt(baX * baX + baY * baY);
        double magnitudeBC = Math.sqrt(bcX * bcX + bcY * bcY);
        if (magnitudeBA == 0 || magnitudeBC == 0) {
            return 0;
        }
        double cosAngle = dotProduct / (magnitudeBA * magnitudeBC);
        // å¤„理浮点误差
        cosAngle = Math.max(-1.0, Math.min(1.0, cosAngle));
        return Math.acos(cosAngle);
    }
    /**
     * é“格拉斯-普克算法(迭代实现,避免递归栈溢出)
     */
    private List<Point> douglasPeuckerSimplify(List<Point> points, double epsilon) {
        if (points.size() <= 2) {
            return new ArrayList<>(points);
        }
        // ä½¿ç”¨ä½æ ‡è®°æ•°ç»„,比递归更节省内存
        boolean[] keep = new boolean[points.size()];
        keep[0] = keep[points.size() - 1] = true;
        // ä½¿ç”¨æ ˆæ¥æ¨¡æ‹Ÿé€’å½’
        LinkedList<int[]> stack = new LinkedList<>();
        stack.push(new int[]{0, points.size() - 1});
        while (!stack.isEmpty()) {
            int[] range = stack.pop();
            int start = range[0];
            int end = range[1];
            if (end - start < 2) {
                continue;
            }
            double maxDistance = 0;
            int maxIndex = start;
            Point startPoint = points.get(start);
            Point endPoint = points.get(end);
            LineSegment segment = new LineSegment(startPoint, endPoint);
            // å¯»æ‰¾ç¦»çº¿æ®µæœ€è¿œçš„点
            for (int i = start + 1; i < end; i++) {
                double distance = segment.perpendicularDistance(points.get(i));
                if (distance > maxDistance) {
                    maxDistance = distance;
                    maxIndex = i;
                }
            }
            // å¦‚果最远点距离大于容差,则处理两侧
            if (maxDistance > epsilon) {
                keep[maxIndex] = true;
                if (maxIndex - start > 1) {
                    stack.push(new int[]{start, maxIndex});
                }
                if (end - maxIndex > 1) {
                    stack.push(new int[]{maxIndex, end});
                }
            }
        }
        // æ”¶é›†ä¿ç•™çš„点
        List<Point> result = new ArrayList<>();
        for (int i = 0; i < points.size(); i++) {
            if (keep[i]) {
                result.add(points.get(i));
            }
        }
        return result;
    }
    /**
     * ç¡®ä¿ç«¯ç‚¹è¢«ä¿ç•™
     */
    private void ensureEndpoints(List<Point> original, List<Point> simplified) {
        if (original.isEmpty() || simplified.isEmpty()) return;
        Point firstOriginal = original.get(0);
        Point lastOriginal = original.get(original.size() - 1);
        // æ£€æŸ¥é¦–点
        if (!simplified.get(0).equals(firstOriginal)) {
            simplified.add(0, firstOriginal);
        }
        // æ£€æŸ¥å°¾ç‚¹
        Point lastSimplified = simplified.get(simplified.size() - 1);
        if (!lastSimplified.equals(lastOriginal)) {
            simplified.add(lastOriginal);
        }
    }
    /**
     * å°†ç‚¹åˆ—表转换为字符串
     */
    private String pointsToString(List<Point> points, int precision) {
        if (points.isEmpty()) {
            return "";
        }
        return points.stream()
            .map(p -> p.toString(precision))
            .collect(Collectors.joining(";"));
    }
    /**
     * æ‰¹å¤„理优化方法
     */
    public List<String> optimizePaths(List<String> pathStrings, OptimizationConfig config) {
        return pathStrings.stream()
            .map(path -> optimizePath(path, config))
            .collect(Collectors.toList());
    }
    /**
     * æ€§èƒ½ç»Ÿè®¡ä¿¡æ¯
     */
    public static class OptimizationStats {
        public final int originalPoints;
        public final int optimizedPoints;
        public final double reductionPercentage;
        public final long processingTimeMs;
        public OptimizationStats(int original, int optimized, long timeMs) {
            this.originalPoints = original;
            this.optimizedPoints = optimized;
            this.reductionPercentage = original > 0 ?
                (1.0 - (double)optimized / original) * 100 : 0;
            this.processingTimeMs = timeMs;
        }
        @Override
        public String toString() {
            return String.format("优化统计: %d -> %d ç‚¹ (减少 %.1f%%),耗时 %dms",
                originalPoints, optimizedPoints, reductionPercentage, processingTimeMs);
        }
    }
    /**
     * å¸¦ç»Ÿè®¡ä¿¡æ¯çš„优化方法
     */
    public OptimizationResult optimizePathWithStats(String pathStr, OptimizationConfig config) {
        long startTime = System.currentTimeMillis();
        if (pathStr == null || pathStr.trim().isEmpty()) {
            return new OptimizationResult("", new OptimizationStats(0, 0, 0));
        }
        List<Point> originalPoints = parsePoints(pathStr);
        String optimizedPath = optimizePath(pathStr, config);
        List<Point> optimizedPoints = parsePoints(optimizedPath);
        long endTime = System.currentTimeMillis();
        OptimizationStats stats = new OptimizationStats(
            originalPoints.size(),
            optimizedPoints.size(),
            endTime - startTime
        );
        return new OptimizationResult(optimizedPath, stats);
    }
    /**
     * ä¼˜åŒ–结果封装类
     */
    public static class OptimizationResult {
        public final String optimizedPath;
        public final OptimizationStats stats;
        public OptimizationResult(String path, OptimizationStats stats) {
            this.optimizedPath = path;
            this.stats = stats;
        }
    }
    /**
     * ç”Ÿæˆæµ‹è¯•路径
     */
    private static String generateTestPath(int pointCount) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < pointCount; i++) {
            sb.append(i).append(",").append(i % 10);
            if (i < pointCount - 1) {
                sb.append(";");
            }
        }
        return sb.toString();
    }
    /**
     * æ€§èƒ½æµ‹è¯•
     */
    private static void testPerformance(WangfanpathJisuan calculator, int iterations) {
        String testPath = generateTestPath(1000);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            calculator.optimizePath(testPath);
        }
        long endTime = System.currentTimeMillis();
        System.out.printf("处理 %d æ¬¡ï¼Œæ¯æ¬¡1000点,总耗时: %dms,平均每次: %.2fms\n",
            iterations, endTime - startTime,
            (double)(endTime - startTime) / iterations);
    }
}
src/publicway/Fanhuibutton.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package publicway;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.geom.RoundRectangle2D;
/**
 * è¿”回按钮工具类
 * æä¾›ç»Ÿä¸€çš„返回按钮创建方法,左侧显示"返回"文字,右侧显示图标
 */
public final class Fanhuibutton {
    // æŒ‰é’®å›ºå®šå°ºå¯¸
    private static final Dimension BUTTON_SIZE = new Dimension(150, 30);
    // åœ†è§’半径
    private static final int CORNER_RADIUS = 8;
    // ä¸»è‰²è°ƒ (绿色)
    private static final Color BASE_COLOR = new Color(46, 139, 87);
    private Fanhuibutton() {
        // å·¥å…·ç±»ä¸éœ€è¦å®žä¾‹åŒ–
    }
    /**
     * åˆ›å»ºè¿”回按钮
     * æ ·å¼å‚考 buttonset,显示 "返回 >>"
     *
     * @param listener æŒ‰é’®ç‚¹å‡»äº‹ä»¶ç›‘听器
     * @return é…ç½®å¥½çš„返回按钮
     */
    public static JButton createReturnButton(ActionListener listener) {
        // åˆ›å»ºè‡ªå®šä¹‰ç»˜åˆ¶çš„æŒ‰é’®
        JButton returnButton = new JButton("返回 >>") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D) g.create();
                // å¼€å¯æŠ—锯齿
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                // èŽ·å–æŒ‰é’®çŠ¶æ€
                boolean isPressed = getModel().isPressed();
                boolean isRollover = getModel().isRollover();
                int w = getWidth();
                int h = getHeight();
                // è®¡ç®—当前背景色
                if (isPressed) {
                    g2.setColor(BASE_COLOR.darker());
                } else if (isRollover) {
                    g2.setColor(new Color(BASE_COLOR.getRed(), BASE_COLOR.getGreen(), BASE_COLOR.getBlue(), 220));
                } else {
                    g2.setColor(BASE_COLOR);
                }
                // ç»˜åˆ¶åœ†è§’矩形背景
                g2.fill(new RoundRectangle2D.Double(0, 0, w, h, CORNER_RADIUS, CORNER_RADIUS));
                // ç»˜åˆ¶æ–‡å­—
                g2.setColor(Color.WHITE);
                g2.setFont(getFont());
                FontMetrics fm = g2.getFontMetrics();
                // æ–‡å­—居中
                String text = getText();
                int textX = (w - fm.stringWidth(text)) / 2;
                int textY = (h - fm.getHeight()) / 2 + fm.getAscent();
                g2.drawString(text, textX, textY);
                g2.dispose();
            }
        };
        // è®¾ç½®æŒ‰é’®æ ·å¼å±žæ€§
        returnButton.setFont(new Font("微软雅黑", Font.BOLD, 14));
        returnButton.setForeground(Color.WHITE);
        returnButton.setFocusPainted(false);
        returnButton.setContentAreaFilled(false);
        returnButton.setBorderPainted(false);
        returnButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
        // è®¾ç½®å›ºå®šå¤§å°
        returnButton.setPreferredSize(BUTTON_SIZE);
        returnButton.setMinimumSize(BUTTON_SIZE);
        returnButton.setMaximumSize(BUTTON_SIZE);
        // æ·»åŠ ç‚¹å‡»äº‹ä»¶
        if (listener != null) {
            returnButton.addActionListener(listener);
        }
        return returnButton;
    }
}
src/zhuye/Shouye.java
@@ -2125,8 +2125,13 @@
            updateStopButtonIcon();
        }
        if (statusLabel != null) {
            // å¦‚果是往返路径绘制,退出时恢复为"待机"
            if (returnPathDrawer != null && returnPathDrawer.isActive()) {
                statusLabel.setText("待机");
            } else {
            statusLabel.setText(storedStatusBeforeDrawing != null ? storedStatusBeforeDrawing : "待机");
        }
        }
        storedStatusBeforeDrawing = null;
    }
@@ -2437,6 +2442,12 @@
        mapRenderer.setHandheldMowerIconActive(active);
    }
    public void setStatusLabelText(String text) {
        if (statusLabel != null) {
            statusLabel.setText(text);
        }
    }
    public boolean startMowerBoundaryCapture() {
        if (mapRenderer == null) {
            return false;
@@ -3080,10 +3091,14 @@
        if (drawingControlModeActive) {
            updateDrawingControlButtonLabels();
            if (statusLabel != null) {
                if (returnPathDrawer != null && returnPathDrawer.isActive()) {
                    statusLabel.setText("正在绘制往返路径");
                } else {
                statusLabel.setText(paused ? "绘制暂停" : "绘制中");
            }
        }
    }
    }
    private void toggleDrawingPause() {
        applyDrawingPauseState(!drawingPaused, true);
@@ -3106,7 +3121,9 @@
        
        // æ˜¾ç¤º"正在绘制边界"提示
        if (drawingBoundaryLabel != null) {
            drawingBoundaryLabel.setVisible(true);
            // å¦‚果是往返路径绘制,不显示此标签(状态栏已显示"正在绘制往返路径")
            boolean isReturnPathDrawing = returnPathDrawer != null && returnPathDrawer.isActive();
            drawingBoundaryLabel.setVisible(!isReturnPathDrawing);
        }
        boolean enableCircleGuidance = drawingShape != null
@@ -4149,13 +4166,14 @@
    /**
     * å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
     * @param finishCallback å®Œæˆç»˜åˆ¶æ—¶çš„回调
     * @param isHandheld æ˜¯å¦ä½¿ç”¨æ‰‹æŒè®¾å¤‡æ¨¡å¼
     * @return æ˜¯å¦æˆåŠŸå¯åŠ¨
     */
    public boolean startReturnPathDrawing(Runnable finishCallback) {
    public boolean startReturnPathDrawing(Runnable finishCallback, boolean isHandheld) {
        if (returnPathDrawer == null) {
            return false;
        }
        return returnPathDrawer.start(finishCallback);
        return returnPathDrawer.start(finishCallback, isHandheld);
    }
    
    /**
@@ -4223,12 +4241,12 @@
        
        // åˆ›å»ºæˆ–显示返回按钮
        if (pathPreviewReturnButton == null) {
            pathPreviewReturnButton = publicway.buttonset.createStyledButton("返回", null);
            pathPreviewReturnButton.setToolTipText("返回绘制页面");
            pathPreviewReturnButton.addActionListener(e -> {
            // ä½¿ç”¨ Fanhuibutton åˆ›å»ºè¿”回按钮
            pathPreviewReturnButton = publicway.Fanhuibutton.createReturnButton(e -> {
                // åœæ­¢é¢„览
                stopReturnPathPreview();
            });
            pathPreviewReturnButton.setToolTipText("返回绘制页面");
        }
        
        // éšè—å…¶ä»–悬浮按钮
src/zhuye/WangfanDraw.java
@@ -23,6 +23,7 @@
    // ç»˜åˆ¶çŠ¶æ€
    private boolean drawingActive = false;
    private boolean paused = false;
    private boolean isHandheldMode = false;  // æ˜¯å¦ä½¿ç”¨æ‰‹æŒè®¾å¤‡æ¨¡å¼
    
    // åŸºå‡†åæ ‡
    private double[] baseLatLon;
@@ -63,9 +64,10 @@
    /**
     * å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
     * @param finishCallback å®Œæˆç»˜åˆ¶æ—¶çš„回调
     * @param isHandheld æ˜¯å¦ä½¿ç”¨æ‰‹æŒè®¾å¤‡æ¨¡å¼
     * @return æ˜¯å¦æˆåŠŸå¯åŠ¨
     */
    public boolean start(Runnable finishCallback) {
    public boolean start(Runnable finishCallback, boolean isHandheld) {
        if (mapRenderer == null || helper == null) {
            return false;
        }
@@ -83,6 +85,12 @@
        paused = false;
        this.finishCallback = finishCallback;
        this.baseLatLon = baseLatLonCandidate;
        this.isHandheldMode = isHandheld;
        // å¦‚果是手持设备模式,切换图标
        if (isHandheld && shouye != null) {
            shouye.setHandheldMowerIconActive(true);
        }
        // æ¸…空路径点
        synchronized (returnPathPoints) {
@@ -101,6 +109,12 @@
        if (helper != null) {
            helper.enterDrawingControlMode();
        }
        // è®¾ç½®çŠ¶æ€æç¤ºä¸º"正在绘制往返路径"
        if (shouye != null) {
            shouye.setStatusLabelText("正在绘制往返路径");
        }
        startMonitor();
        return true;
    }
@@ -245,6 +259,12 @@
        if (helper != null) {
            helper.exitDrawingControlMode();
        }
        // æ¢å¤å‰²è‰æœºå›¾æ ‡
        if (isHandheldMode && shouye != null) {
            shouye.setHandheldMowerIconActive(false);
        }
        isHandheldMode = false;
    }
    
    /**