From 6799351be12deb2f713f2c0a2b4c467a6d1098c3 Mon Sep 17 00:00:00 2001
From: 张世豪 <979909237@qq.com>
Date: 星期二, 02 十二月 2025 19:51:00 +0800
Subject: [PATCH] 2025122

---
 dikuai.properties                 |    8 
 image/stop0.png                   |    0 
 src/dikuai/addzhangaiwu.java      |  854 ++++++++++++++++-
 src/zhuye/Bluelink.java           |   34 
 image/start0.png                  |    0 
 src/zhuye/MapRenderer.java        |  335 ++++++
 set.properties                    |    5 
 src/udpdell/UDPServer.java        |   69 
 image/blue.png                    |    0 
 src/zhuye/Shouye.java             |  550 ++++++++++
 src/zhuye/Coordinate.java         |   46 
 Obstacledge.properties            |   13 
 image/bluelink.png                |    0 
 src/dikuai/Dikuaiguanli.java      |  205 +++
 image/stop1.png                   |    0 
 src/set/Sets.java                 |  260 +++++
 src/zhangaiwu/Obstacledraw.java   |   67 +
 src/set/Setsys.java               |   19 
 image/start1.png                  |    0 
 src/zhangaiwu/yulanzhangaiwu.java |  381 +++++++
 20 files changed, 2,625 insertions(+), 221 deletions(-)

diff --git a/Obstacledge.properties b/Obstacledge.properties
index b45db39..ae24933 100644
--- a/Obstacledge.properties
+++ b/Obstacledge.properties
@@ -1,5 +1,5 @@
 # 鍓茶崏鏈哄湴鍧楅殰纰嶇墿閰嶇疆鏂囦欢
-# 鐢熸垚鏃堕棿锛�2025-12-01T18:24:39.295276600
+# 鐢熸垚鏃堕棿锛�2025-12-02T18:52:30.885641500
 # 鍧愭爣绯伙細WGS84锛堝害鍒嗘牸寮忥級
 
 # ============ 鍦板潡鍩哄噯绔欓厤缃� ============
@@ -11,11 +11,8 @@
 # 鏍煎紡锛歱lot.[鍦板潡缂栧彿].obstacle.[闅滅鐗╁悕绉癩.originalCoords=[鍧愭爣涓瞉
 # 鏍煎紡锛歱lot.[鍦板潡缂栧彿].obstacle.[闅滅鐗╁悕绉癩.xyCoords=[鍧愭爣涓瞉
 
-# --- 鍦板潡001鐨勯殰纰嶇墿 ---
-plot.DK-001.obstacle.tree1.shape=0
-plot.DK-001.obstacle.tree1.originalCoords=2324.200373,N;11330.666830,E;2324.200400,N;11330.666850,E
-plot.DK-001.obstacle.tree1.xyCoords=5.20,3.80;7.50,6.20
-plot.DK-001.obstacle.pond.shape=1
-plot.DK-001.obstacle.pond.originalCoords=2324.200500,N;11330.666900,E;2324.200550,N;11330.666950,E;2324.200600,N;11330.666900,E;2324.200550,N;11330.666850,E
-plot.DK-001.obstacle.pond.xyCoords=15.30,20.10;18.70,22.50;22.10,20.10;18.70,17.80
+# --- 鍦板潡DK-001鐨勯殰纰嶇墿 ---
+plot.DK-001.obstacle.123.shape=1
+plot.DK-001.obstacle.123.originalCoords=3949.840734,N;11616.743246,E;3949.835186,N;11616.673827,E;3949.840311,N;11616.618736,E;3949.848849,N;11616.705131,E
+plot.DK-001.obstacle.123.xyCoords=-3.74,-0.68;-102.65,-10.97;-181.14,-1.46;-58.05,14.38
 
diff --git a/dikuai.properties b/dikuai.properties
index fe0c49d..579f23d 100644
--- a/dikuai.properties
+++ b/dikuai.properties
@@ -1,5 +1,5 @@
 #Dikuai Properties
-#Sat Nov 29 13:40:33 CST 2025
+#Tue Dec 02 18:52:30 CST 2025
 DK-001.angleThreshold=-1 
 DK-001.baseStationCoordinates=3949.84110064,N,11616.74587312,E 
 DK-001.boundaryCoordinates=0,0;0.45,-2.86;1.81,-6.44;21.93,-40.79;27.22,-50.65;31.66,-54.38;33.17,-54.12;46.44,-47.04;56.61,-40.86;57.42,-39.55;53.19,-31.57;32.88,2.52;31.55,4.1;29.14,4.33;0,0
@@ -12,9 +12,9 @@
 DK-001.landNumber=DK-001
 DK-001.mowingPattern=-1
 DK-001.mowingWidth=150
-DK-001.obstacleCoordinates=-1
-DK-001.obstacleOriginalCoordinates=-1
+DK-001.obstacleCoordinates=-3.74,-0.68;-102.65,-10.97;-181.14,-1.46;-58.05,14.38
+DK-001.obstacleOriginalCoordinates=3949.8407343,N,11616.7432457,E;3949.8351859,N,11616.6738272,E;3949.8403113,N,11616.6187363,E;3949.8488492,N,11616.7051305,E
 DK-001.plannedPath=1.17,-2.58;30.94,-53.42;30.94,-53.42;32.69,-53.44;32.69,-53.44;1.69,-0.51;1.69,-0.51;3.29,-0.27;3.29,-0.27;34.06,-52.80;34.06,-52.80;35.38,-52.09;35.38,-52.09;4.89,-0.03;4.89,-0.03;6.49,0.21;6.49,0.21;36.70,-51.38;36.70,-51.38;38.03,-50.68;38.03,-50.68;8.09,0.44;8.09,0.44;9.68,0.68;9.68,0.68;39.35,-49.97;39.35,-49.97;40.68,-49.26;40.68,-49.26;11.28,0.92;11.28,0.92;12.88,1.16;12.88,1.16;42.00,-48.56;42.00,-48.56;43.33,-47.85;43.33,-47.85;14.48,1.39;14.48,1.39;16.08,1.63;16.08,1.63;44.65,-47.14;44.65,-47.14;45.98,-46.44;45.98,-46.44;17.68,1.87;17.68,1.87;19.28,2.11;19.28,2.11;47.26,-45.66;47.26,-45.66;48.54,-44.88;48.54,-44.88;20.88,2.34;20.88,2.34;22.48,2.58;22.48,2.58;49.82,-44.11;49.82,-44.11;51.11,-43.33;51.11,-43.33;24.08,2.82;24.08,2.82;25.68,3.06;25.68,3.06;52.39,-42.55;52.39,-42.55;53.67,-41.77;53.67,-41.77;27.28,3.29;27.28,3.29;28.87,3.53;28.87,3.53;54.95,-40.99;54.95,-40.99;56.18,-40.12;56.18,-40.12;30.67,3.43\# 瑙勫垝璺緞鍧愭爣
 DK-001.returnPointCoordinates=-1
-DK-001.updateTime=2025-11-27 17\:26\:45
+DK-001.updateTime=2025-12-02 18\:52\:30
 DK-001.userId=-1
diff --git a/image/blue.png b/image/blue.png
new file mode 100644
index 0000000..61a42e9
--- /dev/null
+++ b/image/blue.png
Binary files differ
diff --git a/image/bluelink.png b/image/bluelink.png
new file mode 100644
index 0000000..daf5994
--- /dev/null
+++ b/image/bluelink.png
Binary files differ
diff --git a/image/start0.png b/image/start0.png
new file mode 100644
index 0000000..224fda6
--- /dev/null
+++ b/image/start0.png
Binary files differ
diff --git a/image/start1.png b/image/start1.png
new file mode 100644
index 0000000..1884d18
--- /dev/null
+++ b/image/start1.png
Binary files differ
diff --git a/image/stop0.png b/image/stop0.png
new file mode 100644
index 0000000..35e9630
--- /dev/null
+++ b/image/stop0.png
Binary files differ
diff --git a/image/stop1.png b/image/stop1.png
new file mode 100644
index 0000000..28f85d7
--- /dev/null
+++ b/image/stop1.png
Binary files differ
diff --git a/set.properties b/set.properties
index 66e1db7..3eb7238 100644
--- a/set.properties
+++ b/set.properties
@@ -1,7 +1,8 @@
 #Mower Configuration Properties - Updated
-#Sat Nov 29 14:05:32 CST 2025
+#Tue Dec 02 15:25:38 CST 2025
 appVersion=-1
 cuttingWidth=200
 firmwareVersion=-1
-mowerId=1124
+handheldMarkerId=2354
+mowerId=7785
 simCardNumber=-1
diff --git a/src/dikuai/Dikuaiguanli.java b/src/dikuai/Dikuaiguanli.java
index 94a3d1c..a01435e 100644
--- a/src/dikuai/Dikuaiguanli.java
+++ b/src/dikuai/Dikuaiguanli.java
@@ -15,6 +15,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collections;
 
 import zhangaiwu.AddDikuai;
 import zhangaiwu.Obstacledge;
@@ -61,6 +62,7 @@
 	private ImageIcon boundaryVisibleIcon;
 	private ImageIcon boundaryHiddenIcon;
 	private static final int BOUNDARY_TOGGLE_ICON_SIZE = 48;
+	private Map<String, ObstacleSummary> obstacleSummaryCache = Collections.emptyMap();
 
 	public Dikuaiguanli(String landNumber) {
 		latestInstance = this;
@@ -136,13 +138,15 @@
 		cardsPanel.removeAll();
 		
 		Map<String, Dikuai> allDikuai = Dikuai.getAllDikuai();
-		
+	
 		if (allDikuai.isEmpty()) {
+			obstacleSummaryCache = Collections.emptyMap();
 			// 鏄剧ず绌虹姸鎬�
 			JPanel emptyPanel = createEmptyStatePanel();
 			cardsPanel.add(emptyPanel);
 			setCurrentWorkLand(null, null);
 		} else {
+			obstacleSummaryCache = loadObstacleSummaries();
 			if (allDikuai.size() == 1) {
 				Dikuai onlyDikuai = allDikuai.values().iterator().next();
 				setCurrentWorklandIfNeeded(onlyDikuai);
@@ -241,12 +245,12 @@
 		contentPanel.add(boundaryPanel);
 	contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
 		
-		// 闅滅鐗╁潗鏍囷紙甯︽柊澧炲拰鏌ョ湅鎸夐挳锛�
-		JPanel obstaclePanel = createCardInfoItemWithButton("闅滅鐗╁潗鏍�:",
-			getTruncatedValue(dikuai.getObstacleCoordinates(), 12, "鏈缃�"),
+		ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber());
+		JPanel obstaclePanel = createCardInfoItemWithButton("闅滅鐗�:",
+			obstacleSummary.buildDisplayValue(),
 			"鏂板",
 			e -> addNewObstacle(dikuai));
-		setInfoItemTooltip(obstaclePanel, dikuai.getObstacleCoordinates());
+		setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip());
 		contentPanel.add(obstaclePanel);
 	contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
 
@@ -668,9 +672,14 @@
 				String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null;
 				String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null;
 				String obstacles = (dikuai != null) ? dikuai.getObstacleCoordinates() : null;
+				List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : null;
 				renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName);
 				renderer.setCurrentPlannedPath(plannedPath);
-				renderer.setCurrentObstacles(obstacles, landNumber);
+				if (configuredObstacles != null) {
+					renderer.setCurrentObstacles(configuredObstacles, landNumber);
+				} else {
+					renderer.setCurrentObstacles(obstacles, landNumber);
+				}
 				boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false);
 				renderer.setBoundaryPointsVisible(showBoundaryPoints);
 			}
@@ -734,39 +743,103 @@
 		loadDikuaiData();
 	}
 
+	private Map<String, ObstacleSummary> loadObstacleSummaries() {
+		Map<String, ObstacleSummary> summaries = new HashMap<>();
+		try {
+			File configFile = new File("Obstacledge.properties");
+			if (!configFile.exists()) {
+				return summaries;
+			}
+			Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
+			if (!manager.loadFromFile(configFile.getAbsolutePath())) {
+				return summaries;
+			}
+			for (Obstacledge.Plot plot : manager.getPlots()) {
+				if (plot == null) {
+					continue;
+				}
+				String plotId = plot.getPlotId();
+				if (plotId == null || plotId.trim().isEmpty()) {
+					continue;
+				}
+				List<String> names = new ArrayList<>();
+				for (Obstacledge.Obstacle obstacle : plot.getObstacles()) {
+					if (obstacle == null) {
+						continue;
+					}
+					String name = obstacle.getObstacleName();
+					if (name == null) {
+						continue;
+					}
+					String trimmed = name.trim();
+					if (!trimmed.isEmpty()) {
+						names.add(trimmed);
+					}
+				}
+				summaries.put(plotId.trim(), ObstacleSummary.of(names));
+			}
+		} catch (Exception ex) {
+			System.err.println("璇诲彇闅滅鐗╅厤缃け璐�: " + ex.getMessage());
+		}
+		return summaries;
+	}
+
+	private static List<Obstacledge.Obstacle> loadObstaclesFromConfig(String landNumber) {
+		if (landNumber == null || landNumber.trim().isEmpty()) {
+			return Collections.emptyList();
+		}
+		try {
+			File configFile = new File("Obstacledge.properties");
+			if (!configFile.exists()) {
+				return null;
+			}
+			Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
+			if (!manager.loadFromFile(configFile.getAbsolutePath())) {
+				return null;
+			}
+			Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
+			if (plot == null) {
+				return Collections.emptyList();
+			}
+			List<Obstacledge.Obstacle> obstacles = plot.getObstacles();
+			if (obstacles == null || obstacles.isEmpty()) {
+				return Collections.emptyList();
+			}
+			return new ArrayList<>(obstacles);
+		} catch (Exception ex) {
+			System.err.println("璇诲彇闅滅鐗╅厤缃け璐�: " + ex.getMessage());
+			return null;
+		}
+	}
+
+	private ObstacleSummary getObstacleSummaryFromCache(String landNumber) {
+		if (landNumber == null || landNumber.trim().isEmpty()) {
+			return ObstacleSummary.empty();
+		}
+		if (obstacleSummaryCache == null || obstacleSummaryCache.isEmpty()) {
+			return ObstacleSummary.empty();
+		}
+		ObstacleSummary summary = obstacleSummaryCache.get(landNumber.trim());
+		return summary != null ? summary : ObstacleSummary.empty();
+	}
+
 	private List<String> loadObstacleNamesForLand(String landNumber) {
 		List<String> names = new ArrayList<>();
 		if (landNumber == null || landNumber.trim().isEmpty()) {
 			return names;
 		}
-		try {
-			File configFile = new File("Obstacledge.properties");
-			if (!configFile.exists()) {
-				return names;
-			}
-			Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
-			if (!manager.loadFromFile(configFile.getAbsolutePath())) {
-				return names;
-			}
-			Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
-			if (plot == null) {
-				return names;
-			}
-			for (Obstacledge.Obstacle obstacle : plot.getObstacles()) {
-				if (obstacle == null) {
-					continue;
-				}
-				String name = obstacle.getObstacleName();
-				if (name == null) {
-					continue;
-				}
-				String trimmed = name.trim();
-				if (!trimmed.isEmpty() && !names.contains(trimmed)) {
-					names.add(trimmed);
-				}
-			}
-		} catch (Exception ex) {
-			System.err.println("璇诲彇闅滅鐗╅厤缃け璐�: " + ex.getMessage());
+		ObstacleSummary cached = getObstacleSummaryFromCache(landNumber);
+		if (!cached.isEmpty()) {
+			names.addAll(cached.copyNames());
+			return names;
+		}
+		Map<String, ObstacleSummary> latest = loadObstacleSummaries();
+		if (!latest.isEmpty()) {
+			obstacleSummaryCache = latest;
+		}
+		ObstacleSummary refreshed = getObstacleSummaryFromCache(landNumber);
+		if (!refreshed.isEmpty()) {
+			names.addAll(refreshed.copyNames());
 		}
 		return names;
 	}
@@ -887,4 +960,68 @@
 		}
 		boundaryPointVisibility.put(landNumber, visible);
 	}
+
+	private static final class ObstacleSummary {
+		private static final ObstacleSummary EMPTY = new ObstacleSummary(Collections.emptyList());
+		private final List<String> names;
+
+		private ObstacleSummary(List<String> names) {
+			this.names = names;
+		}
+
+		static ObstacleSummary of(List<String> originalNames) {
+			if (originalNames == null || originalNames.isEmpty()) {
+				return empty();
+			}
+			List<String> cleaned = new ArrayList<>();
+			for (String name : originalNames) {
+				if (name == null) {
+					continue;
+				}
+				String trimmed = name.trim();
+				if (trimmed.isEmpty()) {
+					continue;
+				}
+				boolean duplicated = false;
+				for (String existing : cleaned) {
+					if (existing.equalsIgnoreCase(trimmed)) {
+						duplicated = true;
+						break;
+					}
+				}
+				if (!duplicated) {
+					cleaned.add(trimmed);
+				}
+			}
+			if (cleaned.isEmpty()) {
+				return empty();
+			}
+			cleaned.sort(String::compareToIgnoreCase);
+			return new ObstacleSummary(Collections.unmodifiableList(cleaned));
+		}
+
+		static ObstacleSummary empty() {
+			return EMPTY;
+		}
+
+		boolean isEmpty() {
+			return names.isEmpty();
+		}
+
+		int count() {
+			return names.size();
+		}
+
+		String buildDisplayValue() {
+			return count() > 0 ? String.format("闅滅鐗�%d涓�", count()) : "鏆傛棤闅滅鐗�";
+		}
+
+		String buildTooltip() {
+			return count() > 0 ? String.join("锛�", names) : "鏆傛棤闅滅鐗�";
+		}
+
+		List<String> copyNames() {
+			return new ArrayList<>(names);
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/dikuai/addzhangaiwu.java b/src/dikuai/addzhangaiwu.java
index c936a6b..bd7d8e1 100644
--- a/src/dikuai/addzhangaiwu.java
+++ b/src/dikuai/addzhangaiwu.java
@@ -36,15 +36,20 @@
 import javax.swing.JPanel;
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
+import javax.swing.JTextField;
 import javax.swing.border.Border;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
 
 import baseStation.BaseStation;
 import gecaoji.Device;
+import set.Setsys;
 import ui.UIConfig;
 import zhuye.Coordinate;
 import zhuye.Shouye;
 import zhangaiwu.AddDikuai;
 import zhangaiwu.Obstacledge;
+import zhangaiwu.yulanzhangaiwu;
 
 /**
  * 闅滅鐗╂柊澧�/缂栬緫瀵硅瘽妗嗐�傝璁¤瑷�鍙傝�� {@link AddDikuai}锛屾敮鎸侀�氳繃瀹炲湴缁樺埗閲囬泦闅滅鐗╁潗鏍囥��
@@ -61,6 +66,8 @@
     private final Color MEDIUM_GRAY = new Color(233, 236, 239);
     private final Color TEXT_COLOR = new Color(33, 37, 41);
     private final Color LIGHT_TEXT = new Color(108, 117, 125);
+    private final Color DANGER_COLOR = new Color(220, 53, 69);
+    private final Color DANGER_LIGHT = new Color(255, 235, 238);
 
     private final Dikuai targetDikuai;
     private final List<ExistingObstacle> existingObstacles;
@@ -80,6 +87,9 @@
     private JPanel selectedShapePanel;
     private JButton drawButton;
     private JLabel drawingStatusLabel;
+    private JTextField obstacleNameField;
+    private JPanel existingObstacleListPanel;
+    private JPanel step1NextButtonRow;
 
     private int currentStep = 1;
     private boolean drawingInProgress;
@@ -102,8 +112,11 @@
         if (target == null) {
             throw new IllegalArgumentException("targetDikuai 涓嶈兘涓虹┖");
         }
-    this.targetDikuai = target;
-    this.existingObstacles = Collections.unmodifiableList(resolveExistingObstacles(target, obstacleNames));
+        Coordinate.coordinates.clear();
+        Coordinate.setStartSaveGngga(false);
+        Coordinate.clearActiveDeviceIdFilter();
+        this.targetDikuai = target;
+        this.existingObstacles = new ArrayList<>(resolveExistingObstacles(target, obstacleNames));
         initializeUI();
         setupEventHandlers();
         preloadData();
@@ -161,7 +174,11 @@
     infoContainer.add(createInfoRow("鍦板潡缂栧彿锛�", safeValue(targetDikuai.getLandNumber(), "鏈缃�")));
     infoContainer.add(Box.createRigidArea(new Dimension(0, 16)));
     infoContainer.add(createInfoRow("鍦板潡鍚嶇О锛�", safeValue(targetDikuai.getLandName(), "鏈懡鍚�")));
-    infoContainer.add(Box.createRigidArea(new Dimension(0, 20)));
+    infoContainer.add(Box.createRigidArea(new Dimension(0, 16)));
+    infoContainer.add(createObstacleNameRow());
+    infoContainer.add(Box.createRigidArea(new Dimension(0, 12)));
+    infoContainer.add(createStep1NextButtonRow());
+    infoContainer.add(Box.createRigidArea(new Dimension(0, 12)));
     infoContainer.add(buildExistingObstacleSection());
 
     stepPanel.add(infoContainer);
@@ -182,32 +199,64 @@
         section.add(titleLabel);
         section.add(Box.createRigidArea(new Dimension(0, 8)));
 
+        existingObstacleListPanel = new JPanel();
+        existingObstacleListPanel.setLayout(new BoxLayout(existingObstacleListPanel, BoxLayout.Y_AXIS));
+        existingObstacleListPanel.setOpaque(false);
+        existingObstacleListPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+        section.add(existingObstacleListPanel);
+        refreshExistingObstacleList();
+        return section;
+    }
+
+    private JPanel createStep1NextButtonRow() {
+        if (step1NextButtonRow == null) {
+            step1NextButtonRow = new JPanel();
+            step1NextButtonRow.setLayout(new BoxLayout(step1NextButtonRow, BoxLayout.X_AXIS));
+            step1NextButtonRow.setOpaque(false);
+            step1NextButtonRow.setAlignmentX(Component.LEFT_ALIGNMENT);
+            step1NextButtonRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
+            step1NextButtonRow.add(Box.createHorizontalGlue());
+        }
+        return step1NextButtonRow;
+    }
+
+    private void attachNextButtonToStep1Row() {
+        if (step1NextButtonRow == null || nextButton == null) {
+            return;
+        }
+        step1NextButtonRow.removeAll();
+        step1NextButtonRow.add(Box.createHorizontalGlue());
+        nextButton.setAlignmentX(Component.RIGHT_ALIGNMENT);
+        step1NextButtonRow.add(nextButton);
+        step1NextButtonRow.revalidate();
+        step1NextButtonRow.repaint();
+    }
+
+    private void refreshExistingObstacleList() {
+        if (existingObstacleListPanel == null) {
+            return;
+        }
+        existingObstacleListPanel.removeAll();
         if (existingObstacles.isEmpty()) {
             JLabel emptyLabel = new JLabel("鏆傛棤");
             emptyLabel.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 13));
             emptyLabel.setForeground(LIGHT_TEXT);
             emptyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
-            section.add(emptyLabel);
-            return section;
-        }
-
-        JPanel listPanel = new JPanel();
-        listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.Y_AXIS));
-        listPanel.setOpaque(false);
-        listPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
-
-        for (int i = 0; i < existingObstacles.size(); i++) {
-            ExistingObstacle obstacle = existingObstacles.get(i);
-            JPanel row = createObstacleSummaryRow(obstacle);
-            row.setAlignmentX(Component.LEFT_ALIGNMENT);
-            listPanel.add(row);
-            if (i < existingObstacles.size() - 1) {
-                listPanel.add(Box.createRigidArea(new Dimension(0, 6)));
+            existingObstacleListPanel.add(emptyLabel);
+        } else {
+            for (int i = 0; i < existingObstacles.size(); i++) {
+                ExistingObstacle obstacle = existingObstacles.get(i);
+                JPanel row = createObstacleSummaryRow(obstacle);
+                row.setAlignmentX(Component.LEFT_ALIGNMENT);
+                existingObstacleListPanel.add(row);
+                if (i < existingObstacles.size() - 1) {
+                    existingObstacleListPanel.add(Box.createRigidArea(new Dimension(0, 6)));
+                }
             }
         }
-
-        section.add(listPanel);
-        return section;
+        existingObstacleListPanel.revalidate();
+        existingObstacleListPanel.repaint();
     }
 
     private JPanel createObstacleSummaryRow(ExistingObstacle obstacle) {
@@ -225,9 +274,14 @@
         JButton editButton = createInlineButton("淇敼");
         editButton.addActionListener(e -> populateObstacleForEditing(obstacle));
 
+        JButton deleteButton = createInlineIconButton("image/delete.png", "鍒犻櫎闅滅鐗�");
+        deleteButton.addActionListener(e -> handleDeleteExistingObstacle(obstacle));
+
         row.add(infoLabel);
         row.add(Box.createHorizontalGlue());
         row.add(editButton);
+        row.add(Box.createRigidArea(new Dimension(6, 0)));
+        row.add(deleteButton);
         return row;
     }
 
@@ -257,10 +311,56 @@
         return button;
     }
 
+    private JButton createInlineIconButton(String iconPath, String tooltip) {
+        JButton button = new JButton();
+        button.setPreferredSize(new Dimension(32, 28));
+        button.setMinimumSize(new Dimension(32, 28));
+        button.setMaximumSize(new Dimension(32, 28));
+        button.setBackground(WHITE);
+        button.setBorder(BorderFactory.createLineBorder(DANGER_COLOR));
+        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        button.setFocusPainted(false);
+        button.setFocusable(false);
+        button.setOpaque(true);
+        button.setContentAreaFilled(true);
+        if (tooltip != null && !tooltip.trim().isEmpty()) {
+            button.setToolTipText(tooltip);
+        }
+
+        ImageIcon icon = null;
+        if (iconPath != null && !iconPath.trim().isEmpty()) {
+            ImageIcon rawIcon = new ImageIcon(iconPath);
+            if (rawIcon.getIconWidth() > 0 && rawIcon.getIconHeight() > 0) {
+                Image scaled = rawIcon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH);
+                icon = new ImageIcon(scaled);
+            }
+        }
+        if (icon != null) {
+            button.setIcon(icon);
+        } else {
+            button.setText("鍒�");
+            button.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 12));
+            button.setForeground(DANGER_COLOR);
+        }
+
+        button.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseEntered(MouseEvent e) {
+                button.setBackground(DANGER_LIGHT);
+            }
+
+            @Override
+            public void mouseExited(MouseEvent e) {
+                button.setBackground(WHITE);
+            }
+        });
+        return button;
+    }
+
     private String buildObstacleSummaryText(ExistingObstacle obstacle) {
         String name = obstacle.getName();
         String shape = obstacle.getShapeDisplay();
-        String coordPreview = buildCoordinatePreview(obstacle.getCoordinates(), 5);
+    String coordPreview = buildCoordinatePreview(obstacle.getDisplayCoordinates(), 5);
         return String.format(Locale.CHINA, "%s锛�%s锛屽潗鏍�:%s", name, shape, coordPreview);
     }
 
@@ -278,16 +378,70 @@
         return sanitized.substring(0, keepLength) + "...";
     }
 
+    private boolean deleteObstacleFromConfig(String obstacleName) {
+        String landNumber = targetDikuai != null ? targetDikuai.getLandNumber() : null;
+        if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName)) {
+            return false;
+        }
+        try {
+            File configFile = new File("Obstacledge.properties");
+            if (!configFile.exists()) {
+                return false;
+            }
+            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
+            if (!manager.loadFromFile(configFile.getAbsolutePath())) {
+                return false;
+            }
+            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
+            if (plot == null) {
+                return false;
+            }
+            if (!plot.removeObstacleByName(obstacleName.trim())) {
+                return false;
+            }
+            if (!manager.saveToFile(configFile.getAbsolutePath())) {
+                return false;
+            }
+            Dikuai.updateField(landNumber.trim(), "updateTime", currentTime());
+            Dikuai.saveToProperties();
+            Dikuaiguanli.notifyExternalCreation(landNumber.trim());
+            return true;
+        } catch (Exception ex) {
+            System.err.println("鍒犻櫎闅滅鐗╁け璐�: " + ex.getMessage());
+            return false;
+        }
+    }
+
     private void populateObstacleForEditing(ExistingObstacle obstacle) {
         if (obstacle == null) {
             return;
         }
-        String coords = obstacle.getCoordinates();
-        if (isMeaningfulValue(coords)) {
-            formData.put("obstacleCoordinates", coords.trim());
+        String name = obstacle.getName();
+        if (isMeaningfulValue(name)) {
+            formData.put("obstacleName", name.trim());
+            formData.put("editingObstacleName", name.trim());
+        } else {
+            formData.remove("obstacleName");
+            formData.remove("editingObstacleName");
+        }
+        if (obstacleNameField != null) {
+            obstacleNameField.setText(isMeaningfulValue(name) ? name.trim() : "");
+        }
+        String xyCoords = obstacle.getXyCoordinates();
+        String displayCoords = obstacle.getDisplayCoordinates();
+        if (isMeaningfulValue(xyCoords)) {
+            formData.put("obstacleCoordinates", xyCoords.trim());
+        } else if (isMeaningfulValue(displayCoords)) {
+            formData.put("obstacleCoordinates", displayCoords.trim());
         } else {
             formData.remove("obstacleCoordinates");
         }
+        String originalCoords = obstacle.getOriginalCoordinates();
+        if (isMeaningfulValue(originalCoords)) {
+            formData.put("obstacleOriginalCoordinates", originalCoords.trim());
+        } else {
+            formData.remove("obstacleOriginalCoordinates");
+        }
         String shapeKey = obstacle.getShapeKey();
         if (shapeKey != null) {
             JPanel shapePanel = shapeOptionPanels.get(shapeKey);
@@ -297,6 +451,56 @@
         showStep(2);
     }
 
+    private void handleDeleteExistingObstacle(ExistingObstacle obstacle) {
+        if (obstacle == null) {
+            return;
+        }
+        String obstacleName = obstacle.getName();
+        if (!isMeaningfulValue(obstacleName)) {
+            JOptionPane.showMessageDialog(this, "鏃犳硶鍒犻櫎锛氶殰纰嶇墿鍚嶇О鏃犳晥", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+        int choice = JOptionPane.showConfirmDialog(this,
+                "纭畾瑕佸垹闄ら殰纰嶇墿 \"" + obstacleName + "\" 鍚楋紵姝ゆ搷浣滄棤娉曟挙閿�銆�",
+                "鍒犻櫎纭",
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.WARNING_MESSAGE);
+        if (choice != JOptionPane.YES_OPTION) {
+            return;
+        }
+        boolean removedFromConfig = deleteObstacleFromConfig(obstacleName);
+    boolean hasPersistedData = obstacle.hasPersistedData() || obstacle.getShapeKey() != null;
+        if (!removedFromConfig && hasPersistedData) {
+            JOptionPane.showMessageDialog(this, "鍒犻櫎澶辫触锛屾湭鑳藉湪閰嶇疆涓Щ闄よ闅滅鐗┿��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+        if (!existingObstacles.remove(obstacle)) {
+            existingObstacles.removeIf(item -> obstacleName.equals(item != null ? item.getName() : null));
+        }
+        refreshExistingObstacleList();
+        clearEditingStateIfDeleted(obstacleName);
+        JOptionPane.showMessageDialog(this, "闅滅鐗╁凡鍒犻櫎", "鎴愬姛", JOptionPane.INFORMATION_MESSAGE);
+    }
+
+    private void clearEditingStateIfDeleted(String obstacleName) {
+        if (!isMeaningfulValue(obstacleName)) {
+            return;
+        }
+        String editingName = formData.get("editingObstacleName");
+        if (isMeaningfulValue(editingName) && obstacleName.equals(editingName)) {
+            formData.remove("editingObstacleName");
+            formData.remove("obstacleCoordinates");
+            formData.remove("obstacleOriginalCoordinates");
+            if (obstacleNameField != null) {
+                obstacleNameField.setText("");
+            } else {
+                formData.remove("obstacleName");
+            }
+            updateDrawingStatus();
+            updateSaveButtonState();
+        }
+    }
+
     private JPanel createStep2Panel() {
         JPanel stepPanel = new JPanel();
         stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS));
@@ -322,7 +526,7 @@
         stepPanel.add(methodOptionsPanel);
 
         stepPanel.add(Box.createRigidArea(new Dimension(0, 20)));
-        stepPanel.add(createSectionHeader("闅滅鐗╁舰鐘�", "澶氳竟褰㈤渶閲囬泦澶氫釜鐐癸紝鍦嗗舰鍙渶鍦嗗績涓庡渾鍛ㄤ笂涓�鐐�"));
+    stepPanel.add(createSectionHeader("闅滅鐗╁舰鐘�", "澶氳竟褰㈤渶閲囬泦澶氫釜鐐癸紝鍦嗗舰闇�鍦ㄥ渾鍛ㄤ笂閲囬泦鑷冲皯涓変釜鐐�"));
         stepPanel.add(Box.createRigidArea(new Dimension(0, 10)));
 
         shapeOptionsPanel = new JPanel();
@@ -331,7 +535,7 @@
         shapeOptionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
     shapeOptionsPanel.add(createShapeOption("polygon", "澶氳竟褰�", "渚濇閲囬泦杞粨涓婄殑澶氫釜鐐规垨鑰呮部闅滅鐗╄竟缂樿蛋涓�鍦�"));
         shapeOptionsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
-        shapeOptionsPanel.add(createShapeOption("circle", "鍦嗗舰", "鍏堥噰闆嗗渾蹇冨潗鏍囷紝鍐嶉噰闆嗗渾鍛ㄤ笂涓�鐐�"));
+    shapeOptionsPanel.add(createShapeOption("circle", "鍦嗗舰", "娌垮渾鍛ㄤ换鎰忛噰闆嗕笁涓偣鑷姩鐢熸垚鍦�"));
         stepPanel.add(shapeOptionsPanel);
 
         stepPanel.add(Box.createRigidArea(new Dimension(0, 24)));
@@ -366,10 +570,11 @@
 
         buttonPanel.add(prevButton);
         buttonPanel.add(Box.createHorizontalGlue());
-        buttonPanel.add(nextButton);
         buttonPanel.add(Box.createRigidArea(new Dimension(12, 0)));
         buttonPanel.add(saveButton);
 
+        attachNextButtonToStep1Row();
+
         return buttonPanel;
     }
 
@@ -616,6 +821,22 @@
 
         nextButton.addActionListener(e -> {
             if (currentStep == 1) {
+                String name = obstacleNameField != null ? obstacleNameField.getText() : null;
+                if (!isMeaningfulValue(name)) {
+                    JOptionPane.showMessageDialog(this, "璇峰厛濉啓闅滅鐗╁悕绉�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                    if (obstacleNameField != null) {
+                        obstacleNameField.requestFocusInWindow();
+                    }
+                    return;
+                }
+                if (!isObstacleNameUnique(name)) {
+                    JOptionPane.showMessageDialog(this, "闅滅鐗╁悕绉板凡瀛樺湪锛岃杈撳叆鍞竴鍚嶇О", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                    if (obstacleNameField != null) {
+                        obstacleNameField.requestFocusInWindow();
+                    }
+                    return;
+                }
+                formData.put("obstacleName", name.trim());
                 Coordinate.coordinates.clear();
                 showStep(2);
             }
@@ -656,14 +877,28 @@
             return;
         }
 
+    String deviceId = resolveDrawingDeviceId(method);
+    if (!isMeaningfulValue(deviceId)) {
+        String idLabel = "handheld".equalsIgnoreCase(method) ? "鎵嬫寔璁惧缂栧彿" : "鍓茶崏鏈虹紪鍙�";
+        JOptionPane.showMessageDialog(this,
+            "鏈幏鍙栧埌鏈夋晥鐨�" + idLabel + "锛岃鍏堝湪绯荤粺璁剧疆涓畬鎴愰厤缃�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
         activeDrawingShape = shape.toLowerCase(Locale.ROOT);
 
         Coordinate.coordinates.clear();
+        Coordinate.setActiveDeviceIdFilter(deviceId);
         Coordinate.setStartSaveGngga(true);
+        yulanzhangaiwu.startPreview(activeDrawingShape, baseStation);
         drawingInProgress = true;
         drawButton.setText("姝e湪缁樺埗...");
         drawButton.setEnabled(false);
-        drawingStatusLabel.setText("姝e湪閲囬泦闅滅鐗╁潗鏍囷紝璇峰湪涓荤晫闈㈠畬鎴愮粯鍒躲��");
+        if ("circle".equals(activeDrawingShape)) {
+            drawingStatusLabel.setText("姝e湪閲囬泦鍦嗗舰闅滅鐗╋紝璇锋部鍦嗗懆閲囬泦鑷冲皯涓変釜鐐瑰悗鍦ㄤ富鐣岄潰缁撴潫缁樺埗銆�");
+        } else {
+            drawingStatusLabel.setText("姝e湪閲囬泦闅滅鐗╁潗鏍囷紝璇峰湪涓荤晫闈㈠畬鎴愮粯鍒躲��");
+        }
 
         if (activeSession == null) {
             activeSession = new ObstacleDrawingSession();
@@ -692,6 +927,8 @@
 
     public static void finishDrawingSession() {
         Coordinate.setStartSaveGngga(false);
+        Coordinate.clearActiveDeviceIdFilter();
+        yulanzhangaiwu.stopPreview();
 
         Shouye shouye = Shouye.getInstance();
         if (shouye != null) {
@@ -710,11 +947,20 @@
         showDialog(parent, activeSession.target);
     }
 
+    public static String getActiveSessionBaseStation() {
+        if (activeSession != null && isMeaningfulValue(activeSession.baseStation)) {
+            return activeSession.baseStation.trim();
+        }
+        return null;
+    }
+
     /**
      * Stops an in-progress circle capture and reopens the wizard on step 2.
      */
     public static void abortCircleDrawingAndReturn(String message) {
         Coordinate.setStartSaveGngga(false);
+        Coordinate.clearActiveDeviceIdFilter();
+        yulanzhangaiwu.stopPreview();
         Coordinate.coordinates.clear();
 
         if (activeSession == null || activeSession.target == null) {
@@ -750,6 +996,13 @@
             return;
         }
 
+        String originalCoordStr = buildOriginalCoordinateString(captured);
+        if (isMeaningfulValue(originalCoordStr)) {
+            session.data.put("obstacleOriginalCoordinates", originalCoordStr);
+        } else {
+            session.data.remove("obstacleOriginalCoordinates");
+        }
+
         List<double[]> xyPoints = convertToLocalXY(captured, session.baseStation);
         if (xyPoints.isEmpty()) {
             session.captureSuccessful = false;
@@ -759,18 +1012,28 @@
 
         String shape = session.data.get("obstacleShape");
         if ("circle".equals(shape)) {
-            if (xyPoints.size() < 2) {
+            if (xyPoints.size() < 3) {
                 session.captureSuccessful = false;
-                session.captureMessage = "鍦嗗舰闅滅鐗╄嚦灏戦渶瑕佷袱涓噰闆嗙偣锛堝渾蹇冨拰鍦嗗懆鐐癸級";
+                session.captureMessage = "鍦嗗舰闅滅鐗╄嚦灏戦渶瑕佷笁涓噰闆嗙偣锛堝渾鍛ㄤ笂鐨勭偣锛�";
                 return;
             }
-            double[] center = xyPoints.get(0);
-            double[] radiusPoint = xyPoints.get(xyPoints.size() - 1);
+            CircleFitResult circle = fitCircleFromPoints(xyPoints);
+            if (circle == null) {
+                session.captureSuccessful = false;
+                session.captureMessage = "鏃犳硶鏍规嵁閲囬泦鐨勭偣鐢熸垚鍦嗭紝璇风‘淇濋�夋嫨浜嗕笁涓潪鍏辩嚎鐨勫渾鍛ㄧ偣";
+                return;
+            }
+            double[] radiusPoint = pickRadiusPoint(xyPoints, circle);
+            if (radiusPoint == null) {
+                session.captureSuccessful = false;
+                session.captureMessage = "閲囬泦鐨勫渾鍛ㄧ偣寮傚父锛屾棤娉曠敓鎴愬渾";
+                return;
+            }
             String result = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f",
-                    center[0], center[1], radiusPoint[0], radiusPoint[1]);
+                    circle.centerX, circle.centerY, radiusPoint[0], radiusPoint[1]);
             session.data.put("obstacleCoordinates", result);
             session.captureSuccessful = true;
-            session.captureMessage = "宸查噰闆嗗渾褰㈤殰纰嶇墿鍧愭爣";
+            session.captureMessage = "宸查噰闆嗗渾褰㈤殰纰嶇墿锛屽叡 " + xyPoints.size() + " 涓偣";
         } else {
             if (xyPoints.size() < 3) {
                 session.captureSuccessful = false;
@@ -816,6 +1079,67 @@
         return result;
     }
 
+    private static String buildOriginalCoordinateString(List<Coordinate> coords) {
+        if (coords == null || coords.isEmpty()) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (Coordinate coord : coords) {
+            if (coord == null) {
+                continue;
+            }
+            String latToken = sanitizeCoordinateToken(coord.getLatitude());
+            String lonToken = sanitizeCoordinateToken(coord.getLongitude());
+            if (latToken == null || lonToken == null) {
+                continue;
+            }
+            char latDir = sanitizeDirection(coord.getLatDirection(), 'N');
+            char lonDir = sanitizeDirection(coord.getLonDirection(), 'E');
+            if (sb.length() > 0) {
+                sb.append(";");
+            }
+            sb.append(latToken)
+              .append(",")
+              .append(latDir)
+              .append(",")
+              .append(lonToken)
+              .append(",")
+              .append(lonDir);
+        }
+        return sb.length() > 0 ? sb.toString() : null;
+    }
+
+    private static String sanitizeCoordinateToken(String token) {
+        if (token == null) {
+            return null;
+        }
+        String trimmed = token.trim();
+        if (trimmed.isEmpty()) {
+            return null;
+        }
+        try {
+            Double.parseDouble(trimmed);
+            return trimmed;
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private static char sanitizeDirection(String direction, char fallback) {
+        if (direction == null) {
+            return fallback;
+        }
+        String trimmed = direction.trim();
+        if (trimmed.isEmpty()) {
+            return fallback;
+        }
+        char ch = Character.toUpperCase(trimmed.charAt(0));
+        if (ch != 'N' && ch != 'S' && ch != 'E' && ch != 'W') {
+            return fallback;
+        }
+        return ch;
+    }
+
     private static double parseDMToDecimal(String dmm, String direction) {
         if (dmm == null || dmm.trim().isEmpty()) {
             return Double.NaN;
@@ -847,6 +1171,101 @@
         return new double[]{eastMeters, northMeters};
     }
 
+    private static CircleFitResult fitCircleFromPoints(List<double[]> points) {
+        if (points == null || points.size() < 3) {
+            return null;
+        }
+        CircleFitResult best = null;
+        double bestScore = 0.0;
+        int n = points.size();
+        for (int i = 0; i < n - 2; i++) {
+            double[] p1 = points.get(i);
+            for (int j = i + 1; j < n - 1; j++) {
+                double[] p2 = points.get(j);
+                for (int k = j + 1; k < n; k++) {
+                    double[] p3 = points.get(k);
+                    CircleFitResult candidate = circleFromThreePoints(p1, p2, p3);
+                    if (candidate == null || candidate.radius <= 0) {
+                        continue;
+                    }
+                    double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3)));
+                    if (minEdge > bestScore) {
+                        bestScore = minEdge;
+                        best = candidate;
+                    }
+                }
+            }
+        }
+        return best;
+    }
+
+    private static CircleFitResult circleFromThreePoints(double[] p1, double[] p2, double[] p3) {
+        double x1 = p1[0];
+        double y1 = p1[1];
+        double x2 = p2[0];
+        double y2 = p2[1];
+        double x3 = p3[0];
+        double y3 = p3[1];
+
+        double determinant = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
+        if (Math.abs(determinant) < 1e-6) {
+            return null;
+        }
+
+        double x1Sq = x1 * x1 + y1 * y1;
+        double x2Sq = x2 * x2 + y2 * y2;
+        double x3Sq = x3 * x3 + y3 * y3;
+
+        double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / determinant;
+        double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / determinant;
+        double radius = Math.hypot(centerX - x1, centerY - y1);
+
+        if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) {
+            return null;
+        }
+        if (radius < 0.05) {
+            return null;
+        }
+
+        return new CircleFitResult(centerX, centerY, radius, new double[]{x1, y1});
+    }
+
+    private static double[] pickRadiusPoint(List<double[]> points, CircleFitResult circle) {
+        if (circle == null || points == null || points.isEmpty()) {
+            return null;
+        }
+        double[] best = null;
+        double bestDistance = 0.0;
+        for (double[] pt : points) {
+            double dist = Math.hypot(pt[0] - circle.centerX, pt[1] - circle.centerY);
+            if (dist > bestDistance) {
+                bestDistance = dist;
+                best = pt;
+            }
+        }
+        return best;
+    }
+
+    private static double distance(double[] a, double[] b) {
+        double dx = a[0] - b[0];
+        double dy = a[1] - b[1];
+        return Math.hypot(dx, dy);
+    }
+
+    private static final class CircleFitResult {
+        final double centerX;
+        final double centerY;
+        final double radius;
+        final double[] referencePoint;
+
+        CircleFitResult(double centerX, double centerY, double radius, double[] referencePoint) {
+            this.centerX = centerX;
+            this.centerY = centerY;
+            this.radius = radius;
+            this.referencePoint = referencePoint;
+        }
+    }
+
     private List<ExistingObstacle> resolveExistingObstacles(Dikuai target, List<String> providedNames) {
         String landNumber = target != null ? target.getLandNumber() : null;
         Map<String, ExistingObstacle> configMap = loadObstacleDetailsFromConfig(landNumber);
@@ -914,11 +1333,11 @@
                 }
                 String trimmedName = name.trim();
                 Obstacledge.ObstacleShape shape = obstacle.getShape();
-                String xyCoords = obstacle.getXyCoordsString();
-                String original = obstacle.getOriginalCoordsString();
-                String coords = isMeaningfulValue(xyCoords) ? xyCoords.trim()
-                        : (isMeaningfulValue(original) ? original.trim() : "");
-                details.put(trimmedName, new ExistingObstacle(trimmedName, shape, coords));
+        String xyCoords = obstacle.getXyCoordsString();
+        String original = obstacle.getOriginalCoordsString();
+        String xy = isMeaningfulValue(xyCoords) ? xyCoords.trim() : "";
+        String originalTrimmed = isMeaningfulValue(original) ? original.trim() : "";
+        details.put(trimmedName, new ExistingObstacle(trimmedName, shape, xy, originalTrimmed));
             }
         } catch (Exception ex) {
             System.err.println("鍔犺浇闅滅鐗╄鎯呭け璐�: " + ex.getMessage());
@@ -927,10 +1346,9 @@
     }
 
     private void preloadData() {
-        String existing = targetDikuai.getObstacleCoordinates();
-        if (isMeaningfulValue(existing)) {
-            formData.put("obstacleCoordinates", existing.trim());
-        }
+        formData.remove("obstacleCoordinates");
+        formData.remove("obstacleOriginalCoordinates");
+        formData.remove("editingObstacleName");
         updateDrawingStatus();
     }
 
@@ -979,11 +1397,40 @@
 
     private void updateSaveButtonState() {
         if (saveButton != null) {
-            saveButton.setEnabled(isMeaningfulValue(formData.get("obstacleCoordinates")));
+            boolean hasCoords = isMeaningfulValue(formData.get("obstacleCoordinates"));
+            boolean hasName = isMeaningfulValue(formData.get("obstacleName"));
+            boolean enabled = hasCoords && hasName;
+            saveButton.setEnabled(enabled);
+            if (enabled) {
+                saveButton.setBackground(PRIMARY_COLOR);
+                saveButton.setForeground(WHITE);
+                saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+            } else {
+                saveButton.setBackground(MEDIUM_GRAY);
+                saveButton.setForeground(TEXT_COLOR);
+                saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+            }
         }
     }
 
     private boolean validateStep2() {
+        String name = formData.get("obstacleName");
+        if (!isMeaningfulValue(name)) {
+            JOptionPane.showMessageDialog(this, "璇峰~鍐欓殰纰嶇墿鍚嶇О", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            showStep(1);
+            if (obstacleNameField != null) {
+                obstacleNameField.requestFocusInWindow();
+            }
+            return false;
+        }
+        if (!isObstacleNameUnique(name)) {
+            JOptionPane.showMessageDialog(this, "闅滅鐗╁悕绉板凡瀛樺湪锛岃杈撳叆鍞竴鍚嶇О", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            showStep(1);
+            if (obstacleNameField != null) {
+                obstacleNameField.requestFocusInWindow();
+            }
+            return false;
+        }
         if (!isMeaningfulValue(formData.get("obstacleCoordinates"))) {
             JOptionPane.showMessageDialog(this, "璇峰厛瀹屾垚闅滅鐗╃粯鍒�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
             return false;
@@ -995,34 +1442,198 @@
         if (!validateStep2()) {
             return;
         }
-        String coords = formData.get("obstacleCoordinates").trim();
         String landNumber = targetDikuai.getLandNumber();
+        String obstacleName = formData.get("obstacleName");
+        String coordsValue = formData.get("obstacleCoordinates");
+        String originalValue = formData.get("obstacleOriginalCoordinates");
+        String shapeKey = formData.get("obstacleShape");
+        String previousName = formData.get("editingObstacleName");
+
+        if (!isMeaningfulValue(coordsValue)) {
+            JOptionPane.showMessageDialog(this, "闅滅鐗╁潗鏍囨棤鏁�", "閿欒", JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        String coords = coordsValue.trim();
+        String originalCoords = isMeaningfulValue(originalValue) ? originalValue.trim() : null;
+        String trimmedName = isMeaningfulValue(obstacleName) ? obstacleName.trim() : null;
+        if (!isMeaningfulValue(trimmedName)) {
+            JOptionPane.showMessageDialog(this, "闅滅鐗╁悕绉版棤鏁�", "閿欒", JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        formData.put("obstacleName", trimmedName);
+        if (isMeaningfulValue(previousName)) {
+            formData.put("editingObstacleName", trimmedName);
+        }
+
+        if (!persistObstacleToConfig(landNumber, previousName, trimmedName, shapeKey, coords, originalCoords)) {
+            JOptionPane.showMessageDialog(this, "鍐欏叆闅滅鐗╅厤缃け璐ワ紝璇烽噸璇�", "閿欒", JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
         if (!Dikuai.updateField(landNumber, "obstacleCoordinates", coords)) {
             JOptionPane.showMessageDialog(this, "鏃犳硶鏇存柊闅滅鐗╁潗鏍�", "閿欒", JOptionPane.ERROR_MESSAGE);
             return;
         }
+        if (originalCoords != null) {
+            if (!Dikuai.updateField(landNumber, "obstacleOriginalCoordinates", originalCoords)) {
+                JOptionPane.showMessageDialog(this, "鏃犳硶鏇存柊闅滅鐗╁師濮嬪潗鏍�", "閿欒", JOptionPane.ERROR_MESSAGE);
+                return;
+            }
+        } else if (!Dikuai.updateField(landNumber, "obstacleOriginalCoordinates", "-1")) {
+            JOptionPane.showMessageDialog(this, "鏃犳硶閲嶇疆闅滅鐗╁師濮嬪潗鏍�", "閿欒", JOptionPane.ERROR_MESSAGE);
+            return;
+        }
         Dikuai.updateField(landNumber, "updateTime", currentTime());
         Dikuai.saveToProperties();
         Dikuaiguanli.notifyExternalCreation(landNumber);
 
-        JOptionPane.showMessageDialog(this, "闅滅鐗╁潗鏍囧凡淇濆瓨", "鎴愬姛", JOptionPane.INFORMATION_MESSAGE);
+    JOptionPane.showMessageDialog(this, "闅滅鐗╂暟鎹凡淇濆瓨", "鎴愬姛", JOptionPane.INFORMATION_MESSAGE);
         activeSession = null;
         dispose();
     }
 
+    private boolean persistObstacleToConfig(String landNumber, String previousName, String obstacleName,
+                                            String shapeKey, String xyCoords, String originalCoords) {
+        if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName) || !isMeaningfulValue(xyCoords)) {
+            return false;
+        }
+
+        String normalizedLandNumber = landNumber.trim();
+        String normalizedNewName = obstacleName.trim();
+        String normalizedPrevious = isMeaningfulValue(previousName) ? previousName.trim() : null;
+
+        String normalizedXy = xyCoords.trim();
+
+        try {
+            File configFile = new File("Obstacledge.properties");
+            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
+            if (configFile.exists()) {
+                if (!manager.loadFromFile(configFile.getAbsolutePath())) {
+                    return false;
+                }
+            }
+
+            Obstacledge.Plot plot = manager.getPlotById(normalizedLandNumber);
+            if (plot == null) {
+                plot = new Obstacledge.Plot(normalizedLandNumber);
+                manager.addPlot(plot);
+            }
+
+            ensurePlotBaseStation(plot);
+
+            if (normalizedPrevious != null && !normalizedPrevious.equals(normalizedNewName)) {
+                plot.removeObstacleByName(normalizedPrevious);
+            }
+
+            Obstacledge.Obstacle obstacle = plot.getObstacleByName(normalizedNewName);
+            if (obstacle == null) {
+                obstacle = new Obstacledge.Obstacle(normalizedLandNumber, normalizedNewName,
+                        determineObstacleShape(shapeKey, normalizedXy));
+                plot.addObstacle(obstacle);
+            }
+
+            obstacle.setPlotId(normalizedLandNumber);
+            obstacle.setObstacleName(normalizedNewName);
+            obstacle.setShape(determineObstacleShape(shapeKey, normalizedXy));
+
+            obstacle.setXyCoordinates(new ArrayList<Obstacledge.XYCoordinate>());
+            obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>());
+
+            obstacle.setXyCoordsString(normalizedXy);
+
+            if (isMeaningfulValue(originalCoords)) {
+                try {
+                    obstacle.setOriginalCoordsString(originalCoords);
+                } catch (Exception parseEx) {
+                    System.err.println("瑙f瀽闅滅鐗╁師濮嬪潗鏍囧け璐ワ紝灏嗕娇鐢ㄥ崰浣嶅��: " + parseEx.getMessage());
+                    obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>());
+                }
+            }
+
+            ensurePlaceholderOriginalCoords(obstacle);
+
+            return manager.saveToFile(configFile.getAbsolutePath());
+        } catch (Exception ex) {
+            System.err.println("淇濆瓨闅滅鐗╅厤缃け璐�: " + ex.getMessage());
+            return false;
+        }
+    }
+
+    private Obstacledge.ObstacleShape determineObstacleShape(String shapeKey, String xyCoords) {
+        if (isMeaningfulValue(shapeKey)) {
+            String normalized = shapeKey.trim().toLowerCase(Locale.ROOT);
+            if ("circle".equals(normalized) || "鍦嗗舰".equals(normalized) || "0".equals(normalized)) {
+                return Obstacledge.ObstacleShape.CIRCLE;
+            }
+            if ("polygon".equals(normalized) || "澶氳竟褰�".equals(normalized) || "1".equals(normalized)) {
+                return Obstacledge.ObstacleShape.POLYGON;
+            }
+        }
+
+        int pairCount = countCoordinatePairs(xyCoords);
+        if (pairCount <= 0) {
+            return Obstacledge.ObstacleShape.POLYGON;
+        }
+        if (pairCount <= 2) {
+            return Obstacledge.ObstacleShape.CIRCLE;
+        }
+        return Obstacledge.ObstacleShape.POLYGON;
+    }
+
+    private void ensurePlaceholderOriginalCoords(Obstacledge.Obstacle obstacle) {
+        if (obstacle == null) {
+            return;
+        }
+        List<Obstacledge.DMCoordinate> originals = obstacle.getOriginalCoordinates();
+        if (originals != null && !originals.isEmpty()) {
+            return;
+        }
+        List<Obstacledge.XYCoordinate> xyCoords = obstacle.getXyCoordinates();
+        int pointCount = (xyCoords != null && !xyCoords.isEmpty()) ? xyCoords.size() : 1;
+        List<Obstacledge.DMCoordinate> placeholders = new ArrayList<>(pointCount * 2);
+        for (int i = 0; i < pointCount; i++) {
+            placeholders.add(new Obstacledge.DMCoordinate(0.0, 'N'));
+            placeholders.add(new Obstacledge.DMCoordinate(0.0, 'E'));
+        }
+        obstacle.setOriginalCoordinates(placeholders);
+    }
+
+    private void ensurePlotBaseStation(Obstacledge.Plot plot) {
+        if (plot == null) {
+            return;
+        }
+        String existing = plot.getBaseStationString();
+        if (isMeaningfulValue(existing)) {
+            return;
+        }
+        String baseStation = resolveBaseStationCoordinates();
+        if (!isMeaningfulValue(baseStation)) {
+            return;
+        }
+        try {
+            plot.setBaseStationString(baseStation.trim());
+        } catch (Exception ex) {
+            System.err.println("璁剧疆鍩哄噯绔欏潗鏍囧け璐�: " + ex.getMessage());
+        }
+    }
+
     private static final class ExistingObstacle {
         private final String name;
         private final Obstacledge.ObstacleShape shape;
-        private final String coordinates;
+        private final String xyCoordinates;
+        private final String originalCoordinates;
 
-        ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String coordinates) {
+        ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String xyCoordinates, String originalCoordinates) {
             this.name = name != null ? name : "";
             this.shape = shape;
-            this.coordinates = coordinates != null ? coordinates : "";
+            this.xyCoordinates = safeCoordString(xyCoordinates);
+            this.originalCoordinates = safeCoordString(originalCoordinates);
         }
 
         static ExistingObstacle placeholder(String name) {
-            return new ExistingObstacle(name, null, "");
+            return new ExistingObstacle(name, null, "", "");
         }
 
         String getName() {
@@ -1047,7 +1658,38 @@
         }
 
         String getCoordinates() {
-            return coordinates;
+            return getDisplayCoordinates();
+        }
+
+        String getDisplayCoordinates() {
+            return hasText(xyCoordinates) ? xyCoordinates : originalCoordinates;
+        }
+
+        String getXyCoordinates() {
+            return xyCoordinates;
+        }
+
+        String getOriginalCoordinates() {
+            return originalCoordinates;
+        }
+
+        boolean hasPersistedData() {
+            return hasText(xyCoordinates) || hasText(originalCoordinates);
+        }
+
+        private static String safeCoordString(String value) {
+            if (value == null) {
+                return "";
+            }
+            return value.trim();
+        }
+
+        private static boolean hasText(String value) {
+            if (value == null) {
+                return false;
+            }
+            String trimmed = value.trim();
+            return !trimmed.isEmpty() && !"-1".equals(trimmed);
         }
     }
 
@@ -1076,6 +1718,11 @@
         formData.clear();
         formData.putAll(session.data);
 
+        String sessionName = session.data.get("obstacleName");
+        if (obstacleNameField != null) {
+            obstacleNameField.setText(sessionName != null ? sessionName : "");
+        }
+
         String method = session.data.get("drawingMethod");
         if (method != null) {
             JPanel panel = methodOptionPanels.get(method);
@@ -1092,7 +1739,7 @@
             }
         }
 
-        if (session.captureMessage != null) {
+    if (session.captureMessage != null && !session.captureSuccessful) {
             JOptionPane.showMessageDialog(this,
                     session.captureMessage,
                     session.captureSuccessful ? "鎴愬姛" : "鎻愮ず",
@@ -1108,6 +1755,93 @@
         return createInfoRow(label, value, null);
     }
 
+    private JPanel createObstacleNameRow() {
+        JPanel row = new JPanel();
+        row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
+        row.setOpaque(false);
+        row.setAlignmentX(Component.LEFT_ALIGNMENT);
+        row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
+
+        JLabel label = new JLabel("闅滅鐗╁悕绉帮細");
+        label.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 14));
+        label.setForeground(TEXT_COLOR);
+        label.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+        if (obstacleNameField == null) {
+            obstacleNameField = new JTextField();
+            obstacleNameField.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 14));
+            obstacleNameField.setColumns(16);
+            Dimension fieldSize = new Dimension(240, 30);
+            obstacleNameField.setPreferredSize(fieldSize);
+            obstacleNameField.setMinimumSize(fieldSize);
+            obstacleNameField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
+            obstacleNameField.setBorder(BorderFactory.createCompoundBorder(
+                    BorderFactory.createLineBorder(BORDER_COLOR, 1),
+                    BorderFactory.createEmptyBorder(4, 8, 4, 8)));
+            obstacleNameField.getDocument().addDocumentListener(new DocumentListener() {
+                @Override
+                public void insertUpdate(DocumentEvent e) {
+                    handleObstacleNameChanged();
+                }
+
+                @Override
+                public void removeUpdate(DocumentEvent e) {
+                    handleObstacleNameChanged();
+                }
+
+                @Override
+                public void changedUpdate(DocumentEvent e) {
+                    handleObstacleNameChanged();
+                }
+            });
+        }
+
+        String existing = formData.get("obstacleName");
+        if (existing != null && !existing.equals(obstacleNameField.getText())) {
+            obstacleNameField.setText(existing);
+        }
+
+        row.add(label);
+        row.add(Box.createRigidArea(new Dimension(10, 0)));
+        row.add(obstacleNameField);
+        row.add(Box.createHorizontalGlue());
+        return row;
+    }
+
+    private void handleObstacleNameChanged() {
+        if (obstacleNameField == null) {
+            return;
+        }
+        String text = obstacleNameField.getText();
+        if (isMeaningfulValue(text)) {
+            formData.put("obstacleName", text.trim());
+        } else {
+            formData.remove("obstacleName");
+        }
+        updateSaveButtonState();
+    }
+
+    private boolean isObstacleNameUnique(String candidate) {
+        if (!isMeaningfulValue(candidate)) {
+            return false;
+        }
+        String trimmed = candidate.trim().toLowerCase(Locale.ROOT);
+        String original = formData.get("editingObstacleName");
+        if (isMeaningfulValue(original) && trimmed.equals(original.trim().toLowerCase(Locale.ROOT))) {
+            return true;
+        }
+        for (ExistingObstacle obstacle : existingObstacles) {
+            if (obstacle == null) {
+                continue;
+            }
+            String existingName = obstacle.getName();
+            if (isMeaningfulValue(existingName) && trimmed.equals(existingName.trim().toLowerCase(Locale.ROOT))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private JPanel createInfoRow(String label, String value, String tooltip) {
         JPanel row = new JPanel();
         row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
@@ -1206,6 +1940,18 @@
         return null;
     }
 
+    private String resolveDrawingDeviceId(String method) {
+        if (!isMeaningfulValue(method)) {
+            return null;
+        }
+        String key = "handheld".equalsIgnoreCase(method) ? "handheldMarkerId" : "mowerId";
+        String value = Setsys.getPropertyValue(key);
+        if (!isMeaningfulValue(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
     private String safeValue(String value, String fallback) {
         if (!isMeaningfulValue(value)) {
             return fallback;
diff --git a/src/set/Sets.java b/src/set/Sets.java
index d33829b..a5e2863 100644
--- a/src/set/Sets.java
+++ b/src/set/Sets.java
@@ -1,9 +1,12 @@
 package set;
 import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
 
 import java.awt.*;
 import java.awt.event.*;
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 璁剧疆瀵硅瘽妗� - 鍙傝�僑houye.java鏍峰紡
@@ -21,12 +24,15 @@
     
     // 璁剧疆椤圭粍浠�
     private JLabel mowerIdLabel;
+    private JLabel handheldMarkerLabel;
     private JLabel simCardNumberLabel;
     private JLabel firmwareVersionLabel;
     private JLabel appVersionLabel;
     
     private JButton mowerIdEditBtn;
+    private JButton handheldEditBtn;
     private JButton checkUpdateBtn;
+    private JButton feedbackButton;
     
     // 鏁版嵁妯″瀷
     private Setsys setData;
@@ -87,6 +93,11 @@
         mowerIdLabel = (JLabel) mowerIdPanel.getClientProperty("valueLabel");
         mowerIdEditBtn = (JButton) mowerIdPanel.getClientProperty("editButton");
         
+        JPanel handheldPanel = createSettingItemPanel("渚挎惡鎵撶偣鍣ㄧ紪鍙�",
+            setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "鏈缃�", true);
+        handheldMarkerLabel = (JLabel) handheldPanel.getClientProperty("valueLabel");
+        handheldEditBtn = (JButton) handheldPanel.getClientProperty("editButton");
+
         // 鐗╄仈鍗″彿
         JPanel simCardPanel = createSettingItemPanel("鐗╄仈鍗″彿", 
             setData.getSimCardNumber() != null ? setData.getSimCardNumber() : "鏈缃�", false);
@@ -96,13 +107,17 @@
         JPanel firmwarePanel = createSettingItemPanel("鍥轰欢鐗堟湰", 
             setData.getFirmwareVersion() != null ? setData.getFirmwareVersion() : "鏈缃�", false);
         firmwareVersionLabel = (JLabel) firmwarePanel.getClientProperty("valueLabel");
+
+        JPanel feedbackPanel = createFeedbackPanel();
         
         // APP鐗堟湰
         JPanel appVersionPanel = createAppVersionPanel();
         
         addRowWithSpacing(panel, mowerIdPanel);
+        addRowWithSpacing(panel, handheldPanel);
         addRowWithSpacing(panel, simCardPanel);
         addRowWithSpacing(panel, firmwarePanel);
+        addRowWithSpacing(panel, feedbackPanel);
         panel.add(appVersionPanel);
         
         return panel;
@@ -226,6 +241,62 @@
 
         return panel;
     }
+
+    private JPanel createFeedbackPanel() {
+        JPanel panel = new JPanel(new GridBagLayout());
+        panel.setBackground(PANEL_BACKGROUND);
+        panel.setAlignmentX(Component.LEFT_ALIGNMENT);
+        panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, ROW_HEIGHT));
+        panel.setPreferredSize(new Dimension(Integer.MAX_VALUE, ROW_HEIGHT));
+        panel.setMinimumSize(new Dimension(0, ROW_HEIGHT));
+
+        GridBagConstraints gbc = new GridBagConstraints();
+
+        JLabel titleLabel = new JLabel("闂鍙嶉鍜ㄨ");
+        titleLabel.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 14));
+        titleLabel.setForeground(Color.BLACK);
+        titleLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.weightx = 0;
+        gbc.anchor = GridBagConstraints.EAST;
+        gbc.insets = new Insets(0, 0, 0, 12);
+        panel.add(titleLabel, gbc);
+
+        feedbackButton = new JButton("鍙嶉");
+        feedbackButton.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 12));
+        feedbackButton.setBackground(THEME_COLOR);
+        feedbackButton.setForeground(Color.WHITE);
+        feedbackButton.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 20));
+        feedbackButton.setPreferredSize(new Dimension(100, 28));
+        feedbackButton.setMinimumSize(new Dimension(100, 28));
+        feedbackButton.setMaximumSize(new Dimension(100, 28));
+        feedbackButton.setFocusPainted(false);
+
+        feedbackButton.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseEntered(MouseEvent e) {
+                feedbackButton.setBackground(new Color(
+                        Math.max(THEME_COLOR.getRed() - 20, 0),
+                        Math.max(THEME_COLOR.getGreen() - 20, 0),
+                        Math.max(THEME_COLOR.getBlue() - 20, 0)));
+            }
+
+            @Override
+            public void mouseExited(MouseEvent e) {
+                feedbackButton.setBackground(THEME_COLOR);
+            }
+        });
+
+        gbc = new GridBagConstraints();
+        gbc.gridx = 1;
+        gbc.gridy = 0;
+        gbc.weightx = 1.0;
+        gbc.anchor = GridBagConstraints.EAST;
+        panel.add(feedbackButton, gbc);
+
+        return panel;
+    }
     
     private JButton createEditButton() {
         JButton button = new JButton();
@@ -270,6 +341,10 @@
         if (mowerIdLabel != null) {
             mowerIdLabel.setText(setData.getMowerId() != null ? setData.getMowerId() : "鏈缃�");
         }
+
+        if (handheldMarkerLabel != null) {
+            handheldMarkerLabel.setText(setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "鏈缃�");
+        }
         
         // 鏇存柊鐗╄仈鍗″彿鏄剧ず
         if (simCardNumberLabel != null) {
@@ -300,6 +375,14 @@
         if (checkUpdateBtn != null) {
             checkUpdateBtn.addActionListener(e -> checkForUpdates());
         }
+
+        if (handheldEditBtn != null) {
+            handheldEditBtn.addActionListener(e -> editHandheldMarkerId());
+        }
+
+        if (feedbackButton != null) {
+            feedbackButton.addActionListener(e -> showFeedbackDialog());
+        }
         
     }
     
@@ -327,6 +410,183 @@
             }
         }
     }
+
+    private void editHandheldMarkerId() {
+        String currentValue = setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "";
+        String newValue = (String) JOptionPane.showInputDialog(this,
+                "璇疯緭鍏ヤ究鎼烘墦鐐瑰櫒缂栧彿:",
+                "淇敼渚挎惡鎵撶偣鍣ㄧ紪鍙�",
+                JOptionPane.QUESTION_MESSAGE,
+                null,
+                null,
+                currentValue);
+
+        if (newValue == null) {
+            return;
+        }
+
+        newValue = newValue.trim();
+        if (!newValue.isEmpty()) {
+            if (setData.updateProperty("handheldMarkerId", newValue)) {
+                if (handheldMarkerLabel != null) {
+                    handheldMarkerLabel.setText(newValue);
+                }
+                JOptionPane.showMessageDialog(this, "渚挎惡鎵撶偣鍣ㄧ紪鍙锋洿鏂版垚鍔�", "鎴愬姛", JOptionPane.INFORMATION_MESSAGE);
+            } else {
+                JOptionPane.showMessageDialog(this, "渚挎惡鎵撶偣鍣ㄧ紪鍙锋洿鏂板け璐�", "閿欒", JOptionPane.ERROR_MESSAGE);
+            }
+        }
+    }
+
+    private void showFeedbackDialog() {
+    JDialog dialog = new JDialog(this, "闂鍙嶉鍜ㄨ", true);
+    dialog.setLayout(new BorderLayout(0, 12));
+    dialog.setResizable(false);
+    dialog.setPreferredSize(new Dimension(400, 800));
+    dialog.setSize(new Dimension(400, 800));
+    dialog.setMinimumSize(new Dimension(400, 800));
+
+    JPanel content = new JPanel();
+    content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
+    content.setBorder(BorderFactory.createEmptyBorder(20, 24, 20, 24));
+    dialog.add(content, BorderLayout.CENTER);
+
+        JLabel descriptionLabel = new JLabel("闂鎻忚堪锛�");
+        descriptionLabel.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 13));
+        descriptionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+        content.add(descriptionLabel);
+
+        content.add(Box.createRigidArea(new Dimension(0, 8)));
+
+        JTextArea descriptionArea = new JTextArea(5, 30);
+        descriptionArea.setLineWrap(true);
+        descriptionArea.setWrapStyleWord(true);
+        descriptionArea.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 13));
+        JScrollPane scrollPane = new JScrollPane(descriptionArea);
+        scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
+        content.add(scrollPane);
+
+        content.add(Box.createRigidArea(new Dimension(0, 16)));
+
+        JPanel photoControls = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 0));
+        photoControls.setAlignmentX(Component.LEFT_ALIGNMENT);
+        JLabel photoLabel = new JLabel("閫夋嫨鐓х墖锛堟渶澶�6寮狅級锛�");
+        photoLabel.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 13));
+        JButton selectPhotosButton = new JButton("閫夋嫨鐓х墖");
+        selectPhotosButton.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 12));
+        selectPhotosButton.setFocusPainted(false);
+        photoControls.add(photoLabel);
+        photoControls.add(selectPhotosButton);
+    content.add(photoControls);
+
+    content.add(Box.createRigidArea(new Dimension(0, 20)));
+
+        JPanel photoGrid = new JPanel(new GridLayout(2, 3, 12, 12));
+        photoGrid.setAlignmentX(Component.LEFT_ALIGNMENT);
+        photoGrid.setMaximumSize(new Dimension(Integer.MAX_VALUE, 220));
+        List<JLabel> photoSlots = new ArrayList<>();
+        for (int i = 0; i < 6; i++) {
+            JLabel slot = buildPhotoSlot();
+            photoGrid.add(slot);
+            photoSlots.add(slot);
+        }
+        content.add(photoGrid);
+
+        content.add(Box.createRigidArea(new Dimension(0, 20)));
+
+        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+        JButton cancelButton = new JButton("鏀惧純");
+        JButton submitButton = new JButton("鎻愪氦");
+        submitButton.setBackground(THEME_COLOR);
+        submitButton.setForeground(Color.WHITE);
+        submitButton.setFocusPainted(false);
+        cancelButton.setFocusPainted(false);
+
+        buttonPanel.add(cancelButton);
+        buttonPanel.add(submitButton);
+        dialog.add(buttonPanel, BorderLayout.SOUTH);
+
+        List<File> selectedPhotos = new ArrayList<>();
+
+        selectPhotosButton.addActionListener(e -> {
+            JFileChooser chooser = new JFileChooser();
+            chooser.setMultiSelectionEnabled(true);
+            chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+            chooser.setFileFilter(new FileNameExtensionFilter("鍥剧墖鏂囦欢", "jpg", "jpeg", "png", "bmp", "gif", "webp"));
+
+            if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
+                File[] files = chooser.getSelectedFiles();
+                if (files != null) {
+                    if (selectedPhotos.size() >= 6) {
+                        JOptionPane.showMessageDialog(dialog, "宸茶揪鍒�6寮犵収鐗囦笂闄�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                        return;
+                    }
+                    for (File file : files) {
+                        if (selectedPhotos.size() >= 6) {
+                            break;
+                        }
+                        if (file != null && file.exists() && file.isFile()) {
+                            selectedPhotos.add(file);
+                        }
+                    }
+                    updatePhotoPreview(photoSlots, selectedPhotos);
+                }
+            }
+        });
+
+        cancelButton.addActionListener(e -> dialog.dispose());
+
+        submitButton.addActionListener(e -> {
+            String description = descriptionArea.getText() != null ? descriptionArea.getText().trim() : "";
+            if (description.isEmpty()) {
+                JOptionPane.showMessageDialog(dialog, "璇峰~鍐欓棶棰樻弿杩�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                descriptionArea.requestFocusInWindow();
+                return;
+            }
+
+            // 鎻愪氦閫昏緫鍗犱綅
+            JOptionPane.showMessageDialog(dialog,
+                    "鍙嶉宸叉彁浜わ紝鎰熻阿鎮ㄧ殑鏀寔锛乗n鎻忚堪瀛楁暟锛�" + description.length() + "锛岀収鐗囨暟閲忥細" + selectedPhotos.size(),
+                    "鎻愪氦鎴愬姛",
+                    JOptionPane.INFORMATION_MESSAGE);
+            dialog.dispose();
+        });
+
+        dialog.pack();
+        dialog.setLocationRelativeTo(this);
+        dialog.setVisible(true);
+    }
+
+    private JLabel buildPhotoSlot() {
+        JLabel slot = new JLabel();
+        slot.setPreferredSize(new Dimension(100, 100));
+        slot.setMinimumSize(new Dimension(100, 100));
+        slot.setOpaque(true);
+        slot.setBackground(new Color(245, 245, 245));
+        slot.setHorizontalAlignment(SwingConstants.CENTER);
+        slot.setVerticalAlignment(SwingConstants.CENTER);
+        slot.setBorder(BorderFactory.createLineBorder(new Color(220, 220, 220), 1));
+        slot.setText("+");
+        slot.setFont(new Font("寰蒋闆呴粦", Font.BOLD, 18));
+        slot.setForeground(new Color(180, 180, 180));
+        return slot;
+    }
+
+    private void updatePhotoPreview(List<JLabel> slots, List<File> photos) {
+        for (int i = 0; i < slots.size(); i++) {
+            JLabel slot = slots.get(i);
+            if (i < photos.size()) {
+                File photo = photos.get(i);
+                ImageIcon icon = new ImageIcon(photo.getAbsolutePath());
+                Image scaled = icon.getImage().getScaledInstance(100, 100, Image.SCALE_SMOOTH);
+                slot.setIcon(new ImageIcon(scaled));
+                slot.setText(null);
+            } else {
+                slot.setIcon(null);
+                slot.setText("+");
+            }
+        }
+    }
     
     private void checkForUpdates() {
         // 妯℃嫙妫�鏌ユ洿鏂拌繃绋�
diff --git a/src/set/Setsys.java b/src/set/Setsys.java
index ff202d4..c7b75d7 100644
--- a/src/set/Setsys.java
+++ b/src/set/Setsys.java
@@ -7,6 +7,7 @@
     private String mowerId;
     private String cuttingWidth;
     private String simCardNumber;
+    private String handheldMarkerId;
     private String firmwareVersion;
     private String appVersion;
     
@@ -17,10 +18,11 @@
     }
 
     // 甯﹀弬鏋勯�犳柟娉�
-    public Setsys(String mowerId, String cuttingWidth, String simCardNumber, String firmwareVersion, String appVersion) {
+    public Setsys(String mowerId, String cuttingWidth, String simCardNumber, String handheldMarkerId, String firmwareVersion, String appVersion) {
         this.mowerId = mowerId;
         this.cuttingWidth = cuttingWidth;
         this.simCardNumber = simCardNumber;
+        this.handheldMarkerId = handheldMarkerId;
         this.firmwareVersion = firmwareVersion;
         this.appVersion = appVersion;
     }
@@ -50,6 +52,14 @@
         this.simCardNumber = simCardNumber;
     }
 
+    public String getHandheldMarkerId() {
+        return handheldMarkerId;
+    }
+
+    public void setHandheldMarkerId(String handheldMarkerId) {
+        this.handheldMarkerId = handheldMarkerId;
+    }
+
     public String getFirmwareVersion() {
         return firmwareVersion;
     }
@@ -79,6 +89,7 @@
             this.mowerId = "-1".equals(props.getProperty("mowerId")) ? null : props.getProperty("mowerId");
             this.cuttingWidth = "-1".equals(props.getProperty("cuttingWidth")) ? null : props.getProperty("cuttingWidth");
             this.simCardNumber = "-1".equals(props.getProperty("simCardNumber")) ? null : props.getProperty("simCardNumber");
+            this.handheldMarkerId = "-1".equals(props.getProperty("handheldMarkerId")) ? null : props.getProperty("handheldMarkerId");
             this.firmwareVersion = "-1".equals(props.getProperty("firmwareVersion")) ? null : props.getProperty("firmwareVersion");
             this.appVersion = "-1".equals(props.getProperty("appVersion")) ? null : props.getProperty("appVersion");
             
@@ -112,6 +123,9 @@
             case "simCardNumber":
                 this.simCardNumber = value;
                 break;
+            case "handheldMarkerId":
+                this.handheldMarkerId = value;
+                break;
             case "firmwareVersion":
                 this.firmwareVersion = value;
                 break;
@@ -161,6 +175,7 @@
         this.mowerId = null;
         this.cuttingWidth = null;
         this.simCardNumber = null;
+    this.handheldMarkerId = null;
         this.firmwareVersion = null;
         this.appVersion = null;
     }
@@ -173,6 +188,7 @@
         System.out.println("mowerId: " + (mowerId != null ? mowerId : "鏈缃�"));
         System.out.println("cuttingWidth: " + (cuttingWidth != null ? cuttingWidth : "鏈缃�"));
         System.out.println("simCardNumber: " + (simCardNumber != null ? simCardNumber : "鏈缃�"));
+    System.out.println("handheldMarkerId: " + (handheldMarkerId != null ? handheldMarkerId : "鏈缃�"));
         System.out.println("firmwareVersion: " + (firmwareVersion != null ? firmwareVersion : "鏈缃�"));
         System.out.println("appVersion: " + (appVersion != null ? appVersion : "鏈缃�"));
     }
@@ -205,6 +221,7 @@
                 "mowerId='" + mowerId + '\'' +
                 ", cuttingWidth='" + cuttingWidth + '\'' +
                 ", simCardNumber='" + simCardNumber + '\'' +
+                ", handheldMarkerId='" + handheldMarkerId + '\'' +
                 ", firmwareVersion='" + firmwareVersion + '\'' +
                 ", appVersion='" + appVersion + '\'' +
                 '}';
diff --git a/src/udpdell/UDPServer.java b/src/udpdell/UDPServer.java
index 181086a..1ee1bde 100644
--- a/src/udpdell/UDPServer.java
+++ b/src/udpdell/UDPServer.java
@@ -69,7 +69,26 @@
 		}
 	}
 
+	public static void processIncomingMessage(String message) {
+		String[] fields = message.split(",");
+		// 妫�鏌ュ瓧娈垫暟閲忔槸鍚﹀畬鏁�
+		if (fields.length != 21) {
+			System.err.println("Invalid message format, expected 21 fields but got " + fields.length);
+			return;
+		}
 
+		// 妫�鏌ュ寘澶存槸鍚︽纭�
+		if (!fields[0].equals("$GNGGA")) {
+			System.err.println("Invalid message header: " + fields[0]);
+			return;
+		}
+		System.out.println("鏀跺埌浜嗗樊鍒嗘暟鎹細" + message);
+		Coordinate.parseGNGGAToCoordinateList(message);
+		int count = Coordinate.coordinates.size();
+		System.out.println("savenum:" + count);
+
+		Device.updateFromGNGGA(message, fields[15]);
+	}
 
 	private static class PacketProcessor implements Runnable {
 		private final DatagramPacket packet;
@@ -78,60 +97,20 @@
 			this.packet = packet;
 		}
 
+		@Override
 		public void run() {
 			String receivedData = new String(packet.getData(), 0, packet.getLength());
 			// 澶勭悊鍙兘鐨勮繛鍖呮儏鍐�
 			String[] messages = receivedData.split("\\$");
 
 			for (String message : messages) {
-				if (message.isEmpty()) continue;
+				if (message.isEmpty()) {
+					continue;
+				}
 				// 閲嶆柊娣诲姞$绗﹀彿浠ヤ究缁熶竴澶勭悊
 				String fullMessage = "$" + message;
-				processMessage(fullMessage);
+				processIncomingMessage(fullMessage);
 			}
 		}
-
-		private void processMessage(String message) {
-			String[] fields = message.split(",");
-			// 妫�鏌ュ瓧娈垫暟閲忔槸鍚﹀畬鏁�
-			if (fields.length != 21) {
-				System.err.println("Invalid message format, expected 21 fields but got " + fields.length);
-				return;
-			}
-
-			// 妫�鏌ュ寘澶存槸鍚︽纭�
-			if (!fields[0].equals("$GNGGA")) {
-				System.err.println("Invalid message header: " + fields[0]);
-				return;
-			}
-			System.out.println("鏀跺埌浜嗗樊鍒嗘暟鎹細"+message);
-			Coordinate.parseGNGGAToCoordinateList(message);
-			int count= Coordinate.coordinates.size();
-			System.out.println("savenum:"+count);
-
-			Device.updateFromGNGGA(message, fields[15]);            
-			// 鎵撳嵃瑙f瀽鍚庣殑鏁版嵁
-			//System.out.println("UTC鏃堕棿: " + fields[1].toUpperCase());
-			//System.out.println("绾害: " + fields[2].toUpperCase());
-			//System.out.println("绾害鍗婄悆: " + fields[3].toUpperCase());
-			//System.out.println("缁忓害: " + fields[4].toUpperCase());
-			//System.out.println("缁忓害鍗婄悆: " + fields[5].toUpperCase());
-			//System.out.println("瀹氫綅璐ㄩ噺鎸囩ず: " + fields[6].toUpperCase());
-			//System.out.println("鍗槦鏁伴噺: " + fields[7].toUpperCase());
-			//System.out.println("姘村钩绮惧害鍥犲瓙: " + fields[8].toUpperCase());
-			//System.out.println("娴锋嫈楂樺害: " + fields[9].toUpperCase());
-			//System.out.println("娴锋嫈楂樺害鍗曚綅: " + fields[10].toUpperCase());
-			//System.out.println("澶у湴姘村噯闈㈤珮搴�: " + fields[11].toUpperCase());
-			//System.out.println("澶у湴姘村噯闈㈤珮搴﹀崟浣�: " + fields[12].toUpperCase());
-			//System.out.println("宸垎鏃堕棿: " + fields[13].toUpperCase());
-			//System.out.println("鏍¢獙鍜�: " + fields[14].toUpperCase());
-			//System.out.println("璁惧缂栧彿: " + fields[15].toUpperCase());
-			//System.out.println("璁惧鐢甸噺: " + fields[16].toUpperCase());
-			//System.out.println("鍗槦淇″彿寮哄害: " + fields[17].toUpperCase());
-			//System.out.println("淇濈暀浣�1: " + fields[18].toUpperCase());
-			//System.out.println("淇濈暀浣�2: " + fields[19].toUpperCase());
-			//System.out.println("淇濈暀浣�3: " + fields[20].toUpperCase());
-			//System.out.println("----------------------------------------");
-		}
 	}
 }
\ No newline at end of file
diff --git a/src/zhangaiwu/Obstacledraw.java b/src/zhangaiwu/Obstacledraw.java
index a01e0e0..e5d3223 100644
--- a/src/zhangaiwu/Obstacledraw.java
+++ b/src/zhangaiwu/Obstacledraw.java
@@ -210,21 +210,27 @@
             return;
         }
         
-        // 璁$畻鏍囩浣嶇疆锛堜腑蹇冪偣锛�
-        double centerX = 0;
-        double centerY = 0;
-        
-        for (Obstacledge.XYCoordinate coord : xyCoords) {
-            centerX += coord.getX();
-            centerY += coord.getY();
+        double centerX;
+        double centerY;
+
+        Obstacledge.ObstacleShape shape = obstacle.getShape();
+        if (shape == Obstacledge.ObstacleShape.CIRCLE) {
+            Obstacledge.XYCoordinate centerCoord = xyCoords.get(0);
+            centerX = centerCoord.getX();
+            centerY = centerCoord.getY();
+        } else {
+            Point2D.Double centroid = computePolygonCentroid(xyCoords);
+            centerX = centroid.x;
+            centerY = centroid.y;
         }
         
-        centerX /= xyCoords.size();
-        centerY /= xyCoords.size();
-        
-        // 鑾峰彇闅滅鐗╁悕绉板拰褰㈢姸
+        // 鑾峰彇闅滅鐗╁悕绉�
         String obstacleName = obstacle.getObstacleName();
-        String shapeDesc = obstacle.getShape().getDescription();
+        if (obstacleName == null || obstacleName.trim().isEmpty()) {
+            obstacleName = "闅滅鐗�";
+        } else {
+            obstacleName = obstacleName.trim();
+        }
         
         // 璁剧疆瀛椾綋鍜岄鑹�
         g2d.setColor(OBSTACLE_LABEL_COLOR);
@@ -236,7 +242,7 @@
         g2d.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, fontSize));
         
         // 缁樺埗鏍囩
-        String label = String.format("%s(%s)", obstacleName, shapeDesc);
+    String label = obstacleName;
         FontMetrics metrics = g2d.getFontMetrics();
         int textWidth = metrics.stringWidth(label);
         int textHeight = metrics.getHeight();
@@ -247,6 +253,41 @@
         
         g2d.drawString(label, textX, textY);
     }
+
+    private static Point2D.Double computePolygonCentroid(List<Obstacledge.XYCoordinate> xyCoords) {
+        double area = 0.0;
+        double cx = 0.0;
+        double cy = 0.0;
+        int n = xyCoords.size();
+        for (int i = 0; i < n; i++) {
+            Obstacledge.XYCoordinate current = xyCoords.get(i);
+            Obstacledge.XYCoordinate next = xyCoords.get((i + 1) % n);
+            double x0 = current.getX();
+            double y0 = current.getY();
+            double x1 = next.getX();
+            double y1 = next.getY();
+            double cross = x0 * y1 - x1 * y0;
+            area += cross;
+            cx += (x0 + x1) * cross;
+            cy += (y0 + y1) * cross;
+        }
+
+        double areaFactor = area * 0.5;
+        if (Math.abs(areaFactor) < 1e-9) {
+            double avgX = 0.0;
+            double avgY = 0.0;
+            for (Obstacledge.XYCoordinate coord : xyCoords) {
+                avgX += coord.getX();
+                avgY += coord.getY();
+            }
+            int size = Math.max(1, xyCoords.size());
+            return new Point2D.Double(avgX / size, avgY / size);
+        }
+
+        double centroidX = cx / (6.0 * areaFactor);
+        double centroidY = cy / (6.0 * areaFactor);
+        return new Point2D.Double(centroidX, centroidY);
+    }
     
     /**
      * 妫�鏌ョ偣鏄惁鍦ㄩ殰纰嶇墿鍐�
diff --git a/src/zhangaiwu/yulanzhangaiwu.java b/src/zhangaiwu/yulanzhangaiwu.java
new file mode 100644
index 0000000..c79ee83
--- /dev/null
+++ b/src/zhangaiwu/yulanzhangaiwu.java
@@ -0,0 +1,381 @@
+package zhangaiwu;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Path2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import javax.swing.SwingUtilities;
+
+import zhuye.Coordinate;
+import zhuye.Shouye;
+
+/**
+ * 鍦ㄥ湴鍥句笂瀹炴椂棰勮姝e湪缁樺埗鐨勯殰纰嶇墿銆�
+ */
+public final class yulanzhangaiwu {
+    private static final Object LOCK = new Object();
+    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
+    private static final Color PREVIEW_LINE_COLOR = new Color(0, 123, 255, 200);
+    private static final Color PREVIEW_FILL_COLOR = new Color(0, 123, 255, 70);
+    private static final Color PREVIEW_POINT_COLOR = new Color(255, 77, 0, 210);
+    private static final double PREVIEW_POINT_SIZE = 0.18d;
+
+    private static boolean active;
+    private static String shapeKey;
+    private static String baseStation;
+    private static List<double[]> localPoints = Collections.emptyList();
+    private static CircleState circleState;
+
+    private yulanzhangaiwu() {
+    }
+
+    public static void startPreview(String shape, String baseStationCoordinates) {
+        synchronized (LOCK) {
+            active = true;
+            shapeKey = shape != null ? shape.toLowerCase(Locale.ROOT) : null;
+            baseStation = baseStationCoordinates;
+            localPoints = Collections.emptyList();
+            circleState = null;
+        }
+        requestRepaint();
+    }
+
+    public static void stopPreview() {
+        synchronized (LOCK) {
+            active = false;
+            shapeKey = null;
+            baseStation = null;
+            localPoints = Collections.emptyList();
+            circleState = null;
+        }
+        requestRepaint();
+    }
+
+    public static void renderPreview(Graphics2D g2d, double scale) {
+        PreviewSnapshot snapshot = snapshot();
+        if (!snapshot.active) {
+            return;
+        }
+
+        Color originalColor = g2d.getColor();
+        Stroke originalStroke = g2d.getStroke();
+        try {
+            Stroke dashedStroke = new BasicStroke(
+                    (float) (1.6f / scale),
+                    BasicStroke.CAP_ROUND,
+                    BasicStroke.JOIN_ROUND,
+                    1f,
+                    new float[]{(float) (6f / scale), (float) (4f / scale)},
+                    0f
+            );
+            Stroke solidStroke = new BasicStroke((float) (1.4f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
+
+            if ("circle".equals(snapshot.shape) && snapshot.circle != null) {
+                drawCirclePreview(g2d, snapshot, dashedStroke, scale);
+            } else {
+                drawPolygonPreview(g2d, snapshot, dashedStroke, solidStroke, scale);
+            }
+        } finally {
+            g2d.setColor(originalColor);
+            g2d.setStroke(originalStroke);
+        }
+    }
+
+    private static void drawCirclePreview(Graphics2D g2d, PreviewSnapshot snapshot, Stroke dashedStroke, double scale) {
+        CircleState circle = snapshot.circle;
+        if (circle.radius <= 0) {
+            drawSamplePoints(g2d, snapshot.points, scale);
+            return;
+        }
+
+        double diameter = circle.radius * 2.0;
+        double x = circle.centerX - circle.radius;
+        double y = circle.centerY - circle.radius;
+        Shape outline = new Ellipse2D.Double(x, y, diameter, diameter);
+
+        g2d.setColor(PREVIEW_FILL_COLOR);
+        g2d.fill(outline);
+
+        g2d.setColor(PREVIEW_LINE_COLOR);
+        g2d.setStroke(dashedStroke);
+        g2d.draw(outline);
+
+        drawPoint(g2d, circle.centerX, circle.centerY, scale, PREVIEW_POINT_COLOR);
+        if (circle.referencePoint != null) {
+            drawPoint(g2d, circle.referencePoint[0], circle.referencePoint[1], scale, PREVIEW_LINE_COLOR.darker());
+        }
+
+        drawSamplePoints(g2d, snapshot.points, scale);
+    }
+
+    private static void drawPolygonPreview(Graphics2D g2d, PreviewSnapshot snapshot, Stroke dashedStroke, Stroke solidStroke, double scale) {
+        List<double[]> pts = snapshot.points;
+        if (pts.isEmpty()) {
+            return;
+        }
+
+        Path2D.Double path = new Path2D.Double();
+        double[] first = pts.get(0);
+        path.moveTo(first[0], first[1]);
+        for (int i = 1; i < pts.size(); i++) {
+            double[] pt = pts.get(i);
+            path.lineTo(pt[0], pt[1]);
+        }
+
+        if (pts.size() >= 3) {
+            path.closePath();
+            g2d.setColor(PREVIEW_FILL_COLOR);
+            g2d.fill(path);
+        }
+
+        g2d.setColor(PREVIEW_LINE_COLOR);
+        g2d.setStroke(pts.size() >= 3 ? dashedStroke : solidStroke);
+        g2d.draw(path);
+
+        drawSamplePoints(g2d, pts, scale);
+    }
+
+    private static void drawSamplePoints(Graphics2D g2d, List<double[]> points, double scale) {
+        if (points.isEmpty()) {
+            return;
+        }
+        for (double[] pt : points) {
+            drawPoint(g2d, pt[0], pt[1], scale, PREVIEW_POINT_COLOR);
+        }
+    }
+
+    private static void drawPoint(Graphics2D g2d, double x, double y, double scale, Color color) {
+        double size = PREVIEW_POINT_SIZE;
+        double half = size / 2.0;
+        Shape dot = new Ellipse2D.Double(x - half, y - half, size, size);
+        g2d.setColor(color);
+        g2d.fill(dot);
+    }
+
+    private static PreviewSnapshot snapshot() {
+        synchronized (LOCK) {
+            if (!active || shapeKey == null || baseStation == null) {
+                return PreviewSnapshot.inactive();
+            }
+            refreshLocked();
+            return PreviewSnapshot.of(shapeKey, localPoints, circleState);
+        }
+    }
+
+    private static void refreshLocked() {
+        String base = baseStation;
+        if (base == null) {
+            localPoints = Collections.emptyList();
+            circleState = null;
+            return;
+        }
+
+        double[] baseLatLon = parseBaseStation(base);
+        if (baseLatLon == null) {
+            localPoints = Collections.emptyList();
+            circleState = null;
+            return;
+        }
+
+        List<Coordinate> copy;
+        synchronized (Coordinate.coordinates) {
+            copy = new ArrayList<>(Coordinate.coordinates);
+        }
+        if (copy.isEmpty()) {
+            localPoints = Collections.emptyList();
+            circleState = null;
+            return;
+        }
+
+        List<double[]> converted = new ArrayList<>(copy.size());
+        for (Coordinate coord : copy) {
+            double lat = parseDMToDecimal(coord.getLatitude(), coord.getLatDirection());
+            double lon = parseDMToDecimal(coord.getLongitude(), coord.getLonDirection());
+            if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
+                continue;
+            }
+            converted.add(convertLatLonToLocal(lat, lon, baseLatLon[0], baseLatLon[1]));
+        }
+
+        localPoints = converted.isEmpty() ? Collections.emptyList() : converted;
+
+        if ("circle".equals(shapeKey)) {
+            circleState = fitCircle(localPoints);
+        } else {
+            circleState = null;
+        }
+    }
+
+    private static double[] parseBaseStation(String baseStationCoordinates) {
+        String[] parts = baseStationCoordinates.split(",");
+        if (parts.length < 4) {
+            return null;
+        }
+        double lat = parseDMToDecimal(parts[0], parts[1]);
+        double lon = parseDMToDecimal(parts[2], parts[3]);
+        if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
+            return null;
+        }
+        return new double[]{lat, lon};
+    }
+
+    private static double parseDMToDecimal(String dmm, String direction) {
+        if (dmm == null || dmm.trim().isEmpty()) {
+            return Double.NaN;
+        }
+        try {
+            String trimmed = dmm.trim();
+            int dotIndex = trimmed.indexOf('.');
+            if (dotIndex < 2) {
+                return Double.NaN;
+            }
+            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
+            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
+            double decimal = degrees + minutes / 60.0;
+            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
+                decimal = -decimal;
+            }
+            return decimal;
+        } catch (NumberFormatException ex) {
+            return Double.NaN;
+        }
+    }
+
+    private static double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
+        double deltaLat = lat - baseLat;
+        double deltaLon = lon - baseLon;
+        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
+        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
+        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
+        return new double[]{eastMeters, northMeters};
+    }
+
+    private static CircleState fitCircle(List<double[]> points) {
+        if (points == null || points.size() < 3) {
+            return null;
+        }
+        CircleState best = null;
+        double bestScore = 0.0;
+        int n = points.size();
+        for (int i = 0; i < n - 2; i++) {
+            double[] p1 = points.get(i);
+            for (int j = i + 1; j < n - 1; j++) {
+                double[] p2 = points.get(j);
+                for (int k = j + 1; k < n; k++) {
+                    double[] p3 = points.get(k);
+                    CircleState candidate = circleFromThreePoints(p1, p2, p3);
+                    if (candidate == null || candidate.radius <= 0) {
+                        continue;
+                    }
+                    double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3)));
+                    if (minEdge > bestScore) {
+                        bestScore = minEdge;
+                        best = candidate;
+                    }
+                }
+            }
+        }
+        return best;
+    }
+
+    private static CircleState circleFromThreePoints(double[] p1, double[] p2, double[] p3) {
+        double x1 = p1[0];
+        double y1 = p1[1];
+        double x2 = p2[0];
+        double y2 = p2[1];
+        double x3 = p3[0];
+        double y3 = p3[1];
+
+        double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
+        double d = 2.0 * a;
+        if (Math.abs(d) < 1e-6) {
+            return null;
+        }
+
+        double x1Sq = x1 * x1 + y1 * y1;
+        double x2Sq = x2 * x2 + y2 * y2;
+        double x3Sq = x3 * x3 + y3 * y3;
+
+        double ux = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d;
+        double uy = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d;
+        double radius = Math.hypot(ux - x1, uy - y1);
+        if (!Double.isFinite(ux) || !Double.isFinite(uy) || !Double.isFinite(radius)) {
+            return null;
+        }
+        if (radius < 0.05) {
+            return null;
+        }
+        double[] reference = new double[]{x1, y1};
+        return new CircleState(ux, uy, radius, reference);
+    }
+
+    private static double distance(double[] a, double[] b) {
+        double dx = a[0] - b[0];
+        double dy = a[1] - b[1];
+        return Math.hypot(dx, dy);
+    }
+
+    private static void requestRepaint() {
+        SwingUtilities.invokeLater(() -> {
+            Shouye shouye = Shouye.getInstance();
+            if (shouye != null) {
+                shouye.repaint();
+            }
+        });
+    }
+
+    private static final class CircleState {
+        final double centerX;
+        final double centerY;
+        final double radius;
+        final double[] referencePoint;
+
+        CircleState(double centerX, double centerY, double radius, double[] referencePoint) {
+            this.centerX = centerX;
+            this.centerY = centerY;
+            this.radius = radius;
+            this.referencePoint = referencePoint;
+        }
+    }
+
+    private static final class PreviewSnapshot {
+        final boolean active;
+        final String shape;
+        final List<double[]> points;
+        final CircleState circle;
+
+        private PreviewSnapshot(boolean active, String shape, List<double[]> points, CircleState circle) {
+            this.active = active;
+            this.shape = shape;
+            this.points = points;
+            this.circle = circle;
+        }
+
+        static PreviewSnapshot inactive() {
+            return new PreviewSnapshot(false, null, Collections.emptyList(), null);
+        }
+
+        static PreviewSnapshot of(String shape, List<double[]> pts, CircleState circle) {
+            List<double[]> safePoints = new ArrayList<>(pts.size());
+            for (double[] pt : pts) {
+                safePoints.add(new double[]{pt[0], pt[1]});
+            }
+            CircleState safeCircle = null;
+            if (circle != null) {
+                double[] refCopy = circle.referencePoint != null
+                        ? new double[]{circle.referencePoint[0], circle.referencePoint[1]}
+                        : null;
+                safeCircle = new CircleState(circle.centerX, circle.centerY, circle.radius, refCopy);
+            }
+            return new PreviewSnapshot(true, shape, safePoints, safeCircle);
+        }
+    }
+}
diff --git a/src/zhuye/Bluelink.java b/src/zhuye/Bluelink.java
new file mode 100644
index 0000000..cc2cc68
--- /dev/null
+++ b/src/zhuye/Bluelink.java
@@ -0,0 +1,34 @@
+package zhuye;
+
+/**
+ * 钃濈墮閫氫俊鍗犱綅瀹炵幇銆傛闈㈢幆澧冧腑浠呬繚鐣� UI 鍥炬爣涓庣姸鎬佸垏鎹紝
+ * 鐪熸鐨勮摑鐗欓�昏緫灏嗗湪 Android 鐜涓嬪疄鐜般��
+ */
+public final class Bluelink {
+    private static boolean connected;
+
+    private Bluelink() {
+    }
+
+    /**
+     * 浼繛鎺ユ柟娉曪紝浠呭垏鎹负鈥滃凡杩炴帴鈥濈姸鎬併��
+     */
+    public static boolean connect() {
+        connected = true;
+        return true;
+    }
+
+    /**
+     * 浼柇寮�鏂规硶锛屼粎鍒囨崲涓衡�滄湭杩炴帴鈥濈姸鎬併��
+     */
+    public static void disconnect() {
+        connected = false;
+    }
+
+    /**
+     * 杩斿洖褰撳墠浼繛鎺ョ姸鎬併��
+     */
+    public static boolean isConnected() {
+        return connected;
+    }
+}
diff --git a/src/zhuye/Coordinate.java b/src/zhuye/Coordinate.java
index cb44765..9fa4fc8 100644
--- a/src/zhuye/Coordinate.java
+++ b/src/zhuye/Coordinate.java
@@ -20,6 +20,8 @@
 		Coordinate.isStartSaveGngga = isStartSaveGngga;
 	}
 
+	private static volatile String activeDeviceIdFilter;
+
 	private String latitude;    // 绾害锛堝害鍒嗘牸寮忥級
 	private String latDirection; // 绾害鏂瑰悜
 	private String longitude;   // 缁忓害锛堝害鍒嗘牸寮忥級
@@ -97,6 +99,11 @@
 						continue;
 					}
 
+					String deviceId = fields.length > 15 ? sanitizeDeviceId(fields[15]) : null;
+					if (!isDeviceAccepted(deviceId)) {
+						continue;
+					}
+
 					// 妫�鏌ュ畾浣嶈川閲�
 					String fixQualityStr = fields[6];
 					if (fixQualityStr.isEmpty()) continue;
@@ -144,6 +151,45 @@
 		}	
 	}
 
+	private static boolean isDeviceAccepted(String deviceId) {
+		String filter = activeDeviceIdFilter;
+		if (filter == null || filter.isEmpty()) {
+			return true;
+		}
+		if (deviceId == null || deviceId.isEmpty()) {
+			return false;
+		}
+		return filter.equalsIgnoreCase(deviceId);
+	}
+
+	private static String sanitizeDeviceId(String raw) {
+		if (raw == null) {
+			return null;
+		}
+		String trimmed = raw.trim();
+		if (trimmed.isEmpty()) {
+			return null;
+		}
+		int starIndex = trimmed.indexOf('*');
+		if (starIndex >= 0) {
+			trimmed = trimmed.substring(0, starIndex);
+		}
+		return trimmed;
+	}
+
+	public static void setActiveDeviceIdFilter(String deviceId) {
+		if (deviceId == null) {
+			activeDeviceIdFilter = null;
+			return;
+		}
+		String trimmed = deviceId.trim();
+		activeDeviceIdFilter = trimmed.isEmpty() ? null : trimmed;
+	}
+
+	public static void clearActiveDeviceIdFilter() {
+		activeDeviceIdFilter = null;
+	}
+
 	/**
 	 * 浠庢牴鐩綍鐨凣NGGA.txt鏂囦欢鍔犺浇鏁版嵁骞惰В鏋愪繚瀛樺埌coordinates闆嗗悎涓�
 	 * 
diff --git a/src/zhuye/MapRenderer.java b/src/zhuye/MapRenderer.java
index aebc291..9cb5475 100644
--- a/src/zhuye/MapRenderer.java
+++ b/src/zhuye/MapRenderer.java
@@ -3,21 +3,27 @@
 import javax.swing.*;
 import java.awt.*;
 import java.awt.event.*;
+import java.awt.FontMetrics;
 import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 import gecaoji.Device;
 import gecaoji.Gecaoji;
 import dikuai.Dikuaiguanli;
 import dikuai.Dikuai;
 import zhangaiwu.Obstacledraw;
 import zhangaiwu.Obstacledge;
+import zhangaiwu.yulanzhangaiwu;
 
 /**
  * 鍦板浘娓叉煋鍣� - 璐熻矗鍧愭爣绯荤粯鍒躲�佽鍥惧彉鎹㈢瓑鍔熻兘
@@ -37,6 +43,8 @@
     private static final Color GRASS_BORDER_COLOR = new Color(60, 179, 113);
     private static final Color BOUNDARY_POINT_COLOR = new Color(128, 0, 128);
     private static final Color BOUNDARY_LABEL_COLOR = Color.BLACK;
+    private static final Color CIRCLE_SAMPLE_COLOR = new Color(220, 20, 60, 230);
+    private static final double CIRCLE_SAMPLE_SIZE = 0.54d;
     private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05;
     private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     
@@ -49,6 +57,7 @@
     private List<Obstacledge.Obstacle> currentObstacles;
     private Rectangle2D.Double obstacleBounds;
     private String selectedObstacleName;
+    private String currentObstacleLandNumber;
     private String boundaryName;
     private boolean boundaryPointsVisible;
     private String currentBoundaryLandNumber;
@@ -65,6 +74,8 @@
     private JLabel realtimeSpeedValueLabel;
     private JLabel headingValueLabel;
     private JLabel updateTimeValueLabel;
+    private CircleCaptureOverlay circleCaptureOverlay;
+    private final List<double[]> circleSampleMarkers = new ArrayList<>();
     
     public MapRenderer(JPanel visualizationPanel) {
         this.visualizationPanel = visualizationPanel;
@@ -218,6 +229,16 @@
             Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName);
         }
 
+        yulanzhangaiwu.renderPreview(g2d, scale);
+
+        if (!circleSampleMarkers.isEmpty()) {
+            drawCircleSampleMarkers(g2d, circleSampleMarkers, scale);
+        }
+
+        if (circleCaptureOverlay != null) {
+            drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale);
+        }
+
         if (hasPlannedPath) {
             drawCurrentPlannedPath(g2d);
         }
@@ -296,6 +317,137 @@
         lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale);
     }
 
+    private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) {
+        if (markers == null || markers.isEmpty()) {
+            return;
+        }
+        Shape markerShape;
+        double half = CIRCLE_SAMPLE_SIZE / 2.0;
+        g2d.setColor(CIRCLE_SAMPLE_COLOR);
+        g2d.setStroke(new BasicStroke((float) (1.2f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+        Font originalFont = g2d.getFont();
+        float baseSize = (float) Math.max(12f / scale, 9f);
+        float reducedSize = Math.max(baseSize / 3f, 3f);
+        Font labelFont = originalFont.deriveFont(reducedSize);
+        g2d.setFont(labelFont);
+        FontMetrics metrics = g2d.getFontMetrics();
+        for (double[] pt : markers) {
+            if (pt == null || pt.length < 2 || !Double.isFinite(pt[0]) || !Double.isFinite(pt[1])) {
+                continue;
+            }
+            double x = pt[0];
+            double y = pt[1];
+            markerShape = new Ellipse2D.Double(x - half, y - half, CIRCLE_SAMPLE_SIZE, CIRCLE_SAMPLE_SIZE);
+            g2d.fill(markerShape);
+            String label = String.format(Locale.US, "%.2f,%.2f", x, y);
+            int textWidth = metrics.stringWidth(label);
+            float textX = (float) (x - textWidth / 2.0);
+            float textY = (float) (y - half - 0.2d) - metrics.getDescent();
+            g2d.setColor(new Color(33, 37, 41, 220));
+            g2d.drawString(label, textX, textY);
+            g2d.setColor(CIRCLE_SAMPLE_COLOR);
+        }
+        g2d.setFont(originalFont);
+    }
+
+    private void drawCircleCaptureOverlay(Graphics2D g2d, CircleCaptureOverlay overlay, double scale) {
+        double diameter = overlay.radius * 2.0;
+        Ellipse2D outline = new Ellipse2D.Double(
+                overlay.centerX - overlay.radius,
+                overlay.centerY - overlay.radius,
+                diameter,
+                diameter);
+
+        Color fillColor = new Color(255, 152, 0, 80);
+        Color borderColor = new Color(255, 87, 34, 230);
+        Color centerColor = new Color(46, 139, 87, 230);
+
+        g2d.setColor(fillColor);
+        g2d.fill(outline);
+
+        g2d.setColor(borderColor);
+        g2d.setStroke(new BasicStroke((float) (1.8f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+        g2d.draw(outline);
+
+        double markerSize = 0.18d;
+        double centerMarkerSize = 0.22d;
+
+        Ellipse2D centerMarker = new Ellipse2D.Double(
+                overlay.centerX - centerMarkerSize / 2.0,
+                overlay.centerY - centerMarkerSize / 2.0,
+                centerMarkerSize,
+                centerMarkerSize);
+        g2d.setColor(centerColor);
+        g2d.fill(centerMarker);
+    }
+
+    public void showCircleCaptureOverlay(double centerX, double centerY, double radius, List<double[]> samplePoints) {
+        List<double[]> copies = new ArrayList<>();
+        if (samplePoints != null) {
+            for (double[] pt : samplePoints) {
+                if (pt == null || pt.length < 2) {
+                    continue;
+                }
+                copies.add(new double[]{pt[0], pt[1]});
+            }
+        }
+        circleCaptureOverlay = new CircleCaptureOverlay(centerX, centerY, radius, copies);
+        updateCircleSampleMarkers(samplePoints);
+        if (visualizationPanel != null) {
+            visualizationPanel.repaint();
+        }
+    }
+
+    public void clearCircleCaptureOverlay() {
+        circleCaptureOverlay = null;
+        if (visualizationPanel != null) {
+            visualizationPanel.repaint();
+        }
+    }
+
+    public void updateCircleSampleMarkers(List<double[]> samplePoints) {
+        circleSampleMarkers.clear();
+        if (samplePoints != null) {
+            for (double[] pt : samplePoints) {
+                if (pt == null || pt.length < 2) {
+                    continue;
+                }
+                double x = pt[0];
+                double y = pt[1];
+                if (!Double.isFinite(x) || !Double.isFinite(y)) {
+                    continue;
+                }
+                circleSampleMarkers.add(new double[]{x, y});
+            }
+        }
+        if (visualizationPanel != null) {
+            visualizationPanel.repaint();
+        }
+    }
+
+    public void clearCircleSampleMarkers() {
+        if (!circleSampleMarkers.isEmpty()) {
+            circleSampleMarkers.clear();
+            if (visualizationPanel != null) {
+                visualizationPanel.repaint();
+            }
+        }
+    }
+
+    private static final class CircleCaptureOverlay {
+        final double centerX;
+        final double centerY;
+        final double radius;
+        final List<double[]> samplePoints;
+
+        CircleCaptureOverlay(double centerX, double centerY, double radius, List<double[]> samplePoints) {
+            this.centerX = centerX;
+            this.centerY = centerY;
+            this.radius = radius;
+            this.samplePoints = samplePoints;
+        }
+    }
+
     private void showMowerInfo() {
         Device device = Device.getGecaoji();
         if (device == null) {
@@ -771,18 +923,24 @@
     }
 
     public void setCurrentObstacles(String obstaclesData, String landNumber) {
-        if (landNumber == null) {
-            clearObstacleData();
-            if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
-                resetView();
-            } else {
-                visualizationPanel.repaint();
-            }
-            return;
-        }
-
         List<Obstacledge.Obstacle> parsed = parseObstacles(obstaclesData, landNumber);
-        if (parsed.isEmpty()) {
+        applyObstaclesToRenderer(parsed, landNumber);
+    }
+
+    public void setCurrentObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+        List<Obstacledge.Obstacle> cloned = cloneObstacles(obstacles, landNumber);
+        applyObstaclesToRenderer(cloned, landNumber);
+    }
+
+    private void applyObstaclesToRenderer(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+        List<Obstacledge.Obstacle> safeList = obstacles != null ? obstacles : Collections.emptyList();
+        String normalizedLand = (landNumber != null) ? landNumber.trim() : null;
+
+        if (normalizedLand != null && !normalizedLand.isEmpty()) {
+            safeList = filterObstaclesForLand(safeList, normalizedLand);
+        }
+
+        if (normalizedLand == null || normalizedLand.isEmpty()) {
             clearObstacleData();
             if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
                 resetView();
@@ -792,8 +950,19 @@
             return;
         }
 
-        currentObstacles = parsed;
-        obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(parsed));
+        if (safeList.isEmpty()) {
+            clearObstacleData();
+            if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
+                resetView();
+            } else {
+                visualizationPanel.repaint();
+            }
+            return;
+        }
+
+        currentObstacles = Collections.unmodifiableList(new ArrayList<>(safeList));
+        currentObstacleLandNumber = normalizedLand;
+        obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(currentObstacles));
         selectedObstacleName = null;
 
         if (!hasRenderableBoundary() && !hasRenderablePlannedPath() && obstacleBounds != null) {
@@ -807,10 +976,150 @@
         }
     }
 
+    private List<Obstacledge.Obstacle> cloneObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+        List<Obstacledge.Obstacle> result = new ArrayList<>();
+        if (obstacles == null || obstacles.isEmpty()) {
+            return result;
+        }
+
+        String normalizedLandNumber = landNumber != null ? landNumber.trim() : null;
+        Set<String> usedNames = new LinkedHashSet<>();
+        int fallbackIndex = 1;
+
+        for (Obstacledge.Obstacle source : obstacles) {
+            if (source == null) {
+                continue;
+            }
+
+            if (normalizedLandNumber != null && !normalizedLandNumber.isEmpty()) {
+                String sourcePlotId = source.getPlotId();
+                if (sourcePlotId != null && !sourcePlotId.trim().isEmpty()
+                        && !normalizedLandNumber.equalsIgnoreCase(sourcePlotId.trim())) {
+                    continue;
+                }
+            }
+
+            Obstacledge.ObstacleShape shape = source.getShape();
+            List<Obstacledge.XYCoordinate> xySource = source.getXyCoordinates();
+            if (shape == null || xySource == null) {
+                continue;
+            }
+
+            List<Obstacledge.XYCoordinate> xyCopy = copyXYCoordinates(xySource);
+            if (shape == Obstacledge.ObstacleShape.CIRCLE && xyCopy.size() < 2) {
+                continue;
+            }
+            if (shape == Obstacledge.ObstacleShape.POLYGON && xyCopy.size() < 3) {
+                continue;
+            }
+
+            String desiredName = source.getObstacleName();
+            if (desiredName == null || desiredName.trim().isEmpty()) {
+                desiredName = "闅滅鐗�" + fallbackIndex++;
+            }
+            String uniqueName = ensureUniqueName(usedNames, desiredName.trim());
+
+            Obstacledge.Obstacle copy = new Obstacledge.Obstacle(
+                normalizedLandNumber != null ? normalizedLandNumber : source.getPlotId(),
+                uniqueName,
+                shape
+            );
+
+            copy.setXyCoordinates(xyCopy);
+
+            List<Obstacledge.DMCoordinate> dmCopy = copyDMCoordinates(source.getOriginalCoordinates());
+            if (dmCopy.isEmpty()) {
+                populateDummyOriginalCoordinates(copy, xyCopy.size());
+            } else {
+                copy.setOriginalCoordinates(dmCopy);
+            }
+
+            result.add(copy);
+        }
+
+        return result;
+    }
+
+    private List<Obstacledge.Obstacle> filterObstaclesForLand(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+        if (obstacles == null || obstacles.isEmpty()) {
+            return Collections.emptyList();
+        }
+        if (landNumber == null || landNumber.trim().isEmpty()) {
+            return Collections.emptyList();
+        }
+        String normalized = landNumber.trim();
+        List<Obstacledge.Obstacle> filtered = new ArrayList<>();
+        for (Obstacledge.Obstacle obstacle : obstacles) {
+            if (obstacle == null) {
+                continue;
+            }
+            String plotId = obstacle.getPlotId();
+            if (plotId == null || plotId.trim().isEmpty()) {
+                filtered.add(obstacle);
+                continue;
+            }
+            if (normalized.equalsIgnoreCase(plotId.trim())) {
+                filtered.add(obstacle);
+            }
+        }
+        return filtered;
+    }
+
+    private String ensureUniqueName(Set<String> usedNames, String preferredName) {
+        String base = (preferredName == null || preferredName.trim().isEmpty()) ? "闅滅鐗�" : preferredName.trim();
+        String normalized = base.toLowerCase(Locale.ROOT);
+        if (usedNames.add(normalized)) {
+            return base;
+        }
+        int suffix = 2;
+        while (true) {
+            String attempt = base + suffix;
+            String attemptKey = attempt.toLowerCase(Locale.ROOT);
+            if (usedNames.add(attemptKey)) {
+                return attempt;
+            }
+            suffix++;
+        }
+    }
+
+    private List<Obstacledge.XYCoordinate> copyXYCoordinates(List<Obstacledge.XYCoordinate> source) {
+        List<Obstacledge.XYCoordinate> copy = new ArrayList<>();
+        if (source == null) {
+            return copy;
+        }
+        for (Obstacledge.XYCoordinate coord : source) {
+            if (coord == null) {
+                continue;
+            }
+            double x = coord.getX();
+            double y = coord.getY();
+            if (!Double.isFinite(x) || !Double.isFinite(y)) {
+                continue;
+            }
+            copy.add(new Obstacledge.XYCoordinate(x, y));
+        }
+        return copy;
+    }
+
+    private List<Obstacledge.DMCoordinate> copyDMCoordinates(List<Obstacledge.DMCoordinate> source) {
+        List<Obstacledge.DMCoordinate> copy = new ArrayList<>();
+        if (source == null) {
+            return copy;
+        }
+        for (Obstacledge.DMCoordinate coord : source) {
+            if (coord == null) {
+                continue;
+            }
+            copy.add(new Obstacledge.DMCoordinate(coord.getDegreeMinute(), coord.getDirection()));
+        }
+        return copy;
+    }
+
     private void clearObstacleData() {
         currentObstacles = null;
         obstacleBounds = null;
         selectedObstacleName = null;
+        currentObstacleLandNumber = null;
     }
 
     private List<Obstacledge.Obstacle> parseObstacles(String obstaclesData, String landNumber) {
diff --git a/src/zhuye/Shouye.java b/src/zhuye/Shouye.java
index 14565cb..4f20092 100644
--- a/src/zhuye/Shouye.java
+++ b/src/zhuye/Shouye.java
@@ -1,6 +1,7 @@
 package zhuye;
 
 import javax.swing.*;
+import javax.swing.Timer;
 
 import baseStation.BaseStation;
 import baseStation.BaseStationDialog;
@@ -14,6 +15,8 @@
 import gecaoji.Device;
 import set.Sets;
 import udpdell.UDPServer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -28,6 +31,7 @@
     private final Color THEME_HOVER_COLOR = new Color(30, 107, 69);
     private final Color BACKGROUND_COLOR = new Color(250, 250, 250);
     private final Color PANEL_BACKGROUND = new Color(255, 255, 255);
+    private final Color STATUS_PAUSE_COLOR = new Color(255, 165, 0);
     
     // 缁勪欢
     private JPanel headerPanel;
@@ -38,11 +42,12 @@
     
     // 鎸夐挳
     private JButton startBtn;
-    private JButton pauseBtn;
+    private JButton stopBtn;
     private JButton legendBtn;
     private JButton remoteBtn;
     private JButton areaSelectBtn;
     private JButton baseStationBtn;
+    private JButton bluetoothBtn;
     
     // 瀵艰埅鎸夐挳
     private JButton homeNavBtn;
@@ -76,6 +81,8 @@
     private ImageIcon pauseIcon;
     private ImageIcon pauseActiveIcon;
     private ImageIcon endIcon;
+    private ImageIcon bluetoothIcon;
+    private ImageIcon bluetoothLinkedIcon;
     private JPanel circleGuidancePanel;
     private JLabel circleGuidanceLabel;
     private JButton circleGuidancePrimaryButton;
@@ -84,6 +91,14 @@
     private JDialog circleGuidanceDialog;
     private boolean circleDialogMode;
     private ComponentAdapter circleDialogOwnerAdapter;
+    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
+    private final List<double[]> circleCapturedPoints = new ArrayList<>();
+    private double[] circleBaseLatLon;
+    private Timer circleDataMonitor;
+    private Coordinate lastCapturedCoordinate;
+    private boolean startButtonShowingPause = true;
+    private boolean stopButtonActive = false;
+    private boolean bluetoothConnected = false;
     
     public Shouye() {
         instance = this;
@@ -163,6 +178,11 @@
         statusLabel = new JLabel("寰呮満");
         statusLabel.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 14));
         statusLabel.setForeground(Color.GRAY);
+        statusLabel.addPropertyChangeListener("text", evt -> {
+            Object newValue = evt.getNewValue();
+            applyStatusLabelColor(newValue instanceof String ? (String) newValue : null);
+        });
+        applyStatusLabelColor(statusLabel.getText());
         
         leftInfoPanel.add(areaNameLabel);
         leftInfoPanel.add(statusLabel);
@@ -171,7 +191,7 @@
         JPanel rightActionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
         rightActionPanel.setBackground(PANEL_BACKGROUND);
         
-        JButton notificationBtn = createIconButton("馃敂", 40);
+    bluetoothBtn = createBluetoothButton();
         
         // 淇敼璁剧疆鎸夐挳锛氫娇鐢ㄥ浘鐗囨浛浠nicode鍥炬爣
         JButton settingsBtn = new JButton();
@@ -203,7 +223,7 @@
         // 娣诲姞璁剧疆鎸夐挳浜嬩欢
         settingsBtn.addActionListener(e -> showSettingsDialog());
         
-        rightActionPanel.add(notificationBtn);
+    rightActionPanel.add(bluetoothBtn);
         rightActionPanel.add(settingsBtn);
         
         headerPanel.add(leftInfoPanel, BorderLayout.WEST);
@@ -260,14 +280,14 @@
         JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 20, 0));
         buttonPanel.setBackground(PANEL_BACKGROUND);
         
-    startBtn = createControlButton("寮�濮�", THEME_COLOR);
-    applyButtonIcon(startBtn, "image/startzuoye.png");
-    pauseBtn = createControlButton("鏆傚仠", Color.ORANGE);
-    applyButtonIcon(pauseBtn, "image/zantingzuoye.png");
-        pauseBtn.setEnabled(false);
+        startBtn = createControlButton("鏆傚仠", THEME_COLOR);
+        updateStartButtonAppearance();
+
+        stopBtn = createControlButton("缁撴潫", Color.ORANGE);
+        updateStopButtonIcon();
         
         buttonPanel.add(startBtn);
-        buttonPanel.add(pauseBtn);
+        buttonPanel.add(stopBtn);
         
         controlPanel.add(buttonPanel, BorderLayout.CENTER);
     }
@@ -310,6 +330,35 @@
         
         return button;
     }
+
+    private JButton createBluetoothButton() {
+        JButton button = new JButton();
+        button.setPreferredSize(new Dimension(40, 40));
+        button.setBackground(PANEL_BACKGROUND);
+        button.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        button.setFocusPainted(false);
+        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        button.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseEntered(MouseEvent e) {
+                button.setBackground(new Color(240, 240, 240));
+            }
+
+            @Override
+            public void mouseExited(MouseEvent e) {
+                button.setBackground(PANEL_BACKGROUND);
+            }
+        });
+        ensureBluetoothIconsLoaded();
+        bluetoothConnected = Bluelink.isConnected();
+        ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
+        if (initialIcon != null) {
+            button.setIcon(initialIcon);
+        } else {
+            button.setText(bluetoothConnected ? "宸茶繛" : "钃濈墮");
+        }
+        return button;
+    }
     
     private JButton createFunctionButton(String text, String icon) {
         JButton button = new JButton("<html><center>" + icon + "<br>" + text + "</center></html>");
@@ -424,6 +473,9 @@
         
         // 鍔熻兘鎸夐挳浜嬩欢
         legendBtn.addActionListener(e -> showLegendDialog());
+        if (bluetoothBtn != null) {
+            bluetoothBtn.addActionListener(e -> toggleBluetoothConnection());
+        }
         remoteBtn.addActionListener(e -> showRemoteControlDialog());
         areaSelectBtn.addActionListener(e -> {
             // 鐐瑰嚮鈥滃湴鍧椻�濈洿鎺ユ墦寮�鍦板潡绠$悊瀵硅瘽妗嗭紙鑻ラ渶瑕佸彲浼犲叆鐗瑰畾鍦板潡缂栧彿锛�
@@ -432,8 +484,8 @@
         baseStationBtn.addActionListener(e -> showBaseStationDialog());
         
         // 鎺у埗鎸夐挳浜嬩欢
-        startBtn.addActionListener(e -> startMowing());
-        pauseBtn.addActionListener(e -> pauseMowing());
+    startBtn.addActionListener(e -> toggleStartPause());
+    stopBtn.addActionListener(e -> handleStopAction());
     }
     
     private void showSettingsDialog() {
@@ -563,16 +615,107 @@
         }
     }
     
-    private void startMowing() {
-        statusLabel.setText("浣滀笟涓�");
-        startBtn.setEnabled(false);
-        pauseBtn.setEnabled(true);
+    private void toggleStartPause() {
+        if (startBtn == null) {
+            return;
+        }
+        startButtonShowingPause = !startButtonShowingPause;
+        if (startButtonShowingPause) {
+            statusLabel.setText("浣滀笟涓�");
+            if (stopButtonActive) {
+                stopButtonActive = false;
+                updateStopButtonIcon();
+            }
+        } else {
+            statusLabel.setText("鏆傚仠涓�");
+        }
+        updateStartButtonAppearance();
     }
-    
-    private void pauseMowing() {
-        statusLabel.setText("鏆傚仠涓�");
-        startBtn.setEnabled(true);
-        pauseBtn.setEnabled(false);
+
+    private void handleStopAction() {
+        stopButtonActive = !stopButtonActive;
+        updateStopButtonIcon();
+        if (stopButtonActive) {
+            statusLabel.setText("宸茬粨鏉�");
+            startButtonShowingPause = false;
+        } else {
+            statusLabel.setText("寰呮満");
+            startButtonShowingPause = true;
+        }
+        updateStartButtonAppearance();
+    }
+
+    private void updateStartButtonAppearance() {
+        if (startBtn == null) {
+            return;
+        }
+        String iconPath = startButtonShowingPause ? "image/start0.png" : "image/start1.png";
+        startBtn.setText(startButtonShowingPause ? "鏆傚仠" : "寮�濮�");
+        applyButtonIcon(startBtn, iconPath);
+    }
+
+    private void updateStopButtonIcon() {
+        if (stopBtn == null) {
+            return;
+        }
+        String iconPath = stopButtonActive ? "image/stop1.png" : "image/stop0.png";
+        applyButtonIcon(stopBtn, iconPath);
+    }
+
+    private void toggleBluetoothConnection() {
+        if (bluetoothBtn == null) {
+            return;
+        }
+        if (Bluelink.isConnected()) {
+            Bluelink.disconnect();
+            bluetoothConnected = false;
+        } else {
+            boolean success = Bluelink.connect();
+            if (success) {
+                bluetoothConnected = true;
+            } else {
+                bluetoothConnected = false;
+                JOptionPane.showMessageDialog(this, "钃濈墮杩炴帴澶辫触锛岃閲嶈瘯", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            }
+        }
+        updateBluetoothButtonIcon();
+    }
+
+    private void updateBluetoothButtonIcon() {
+        if (bluetoothBtn == null) {
+            return;
+        }
+        ensureBluetoothIconsLoaded();
+        bluetoothConnected = Bluelink.isConnected();
+        ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
+        if (icon != null) {
+            bluetoothBtn.setIcon(icon);
+            bluetoothBtn.setText(null);
+        } else {
+            bluetoothBtn.setText(bluetoothConnected ? "宸茶繛" : "钃濈墮");
+        }
+    }
+
+    private void applyStatusLabelColor(String statusText) {
+        if (statusLabel == null) {
+            return;
+        }
+        if ("浣滀笟涓�".equals(statusText)) {
+            statusLabel.setForeground(THEME_COLOR);
+        } else if ("鏆傚仠涓�".equals(statusText)) {
+            statusLabel.setForeground(STATUS_PAUSE_COLOR);
+        } else {
+            statusLabel.setForeground(Color.GRAY);
+        }
+    }
+
+    private void ensureBluetoothIconsLoaded() {
+        if (bluetoothIcon == null) {
+            bluetoothIcon = loadScaledIcon("image/blue.png", 28, 28);
+        }
+        if (bluetoothLinkedIcon == null) {
+            bluetoothLinkedIcon = loadScaledIcon("image/bluelink.png", 28, 28);
+        }
     }
 
     private JButton createFloatingIconButton() {
@@ -657,26 +800,27 @@
     public void showEndDrawingButton(Runnable callback, String drawingShape) {
         endDrawingCallback = callback;
         applyDrawingPauseState(false, false);
-        circleDialogMode = drawingShape != null && "circle".equalsIgnoreCase(drawingShape);
-
-        if (circleDialogMode) {
-            hideFloatingDrawingControls();
-            ensureCircleGuidancePanel();
-            showCircleGuidanceStep(1);
-            visualizationPanel.revalidate();
-            visualizationPanel.repaint();
-            return;
-        }
-
         circleDialogMode = false;
         hideCircleGuidancePanel();
 
         ensureFloatingIconsLoaded();
         ensureFloatingButtonInfrastructure();
 
-        endDrawingButton.setVisible(true);
-        if (drawingPauseButton != null) {
-            drawingPauseButton.setVisible(true);
+        boolean enableCircleGuidance = drawingShape != null
+                && "circle".equalsIgnoreCase(drawingShape.trim());
+        if (enableCircleGuidance) {
+            prepareCircleGuidanceState();
+            showCircleGuidanceStep(1);
+            endDrawingButton.setVisible(false);
+            if (drawingPauseButton != null) {
+                drawingPauseButton.setVisible(false);
+            }
+        } else {
+            clearCircleGuidanceArtifacts();
+            endDrawingButton.setVisible(true);
+            if (drawingPauseButton != null) {
+                drawingPauseButton.setVisible(true);
+            }
         }
 
         floatingButtonPanel.setVisible(true);
@@ -773,20 +917,33 @@
         }
         circleGuidanceStep = step;
         if (step == 1) {
-            circleGuidanceLabel.setText("鏄惁灏嗗綋鍓嶇偣璁剧疆涓哄渾蹇冿紵");
-            circleGuidancePrimaryButton.setText("鏄�");
+            circleGuidanceLabel.setText("閲囬泦绗�1涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�1鐐�");
             circleGuidanceSecondaryButton.setText("杩斿洖");
             circleGuidanceSecondaryButton.setVisible(true);
         } else if (step == 2) {
-            circleGuidanceLabel.setText("鏄惁灏嗗綋鍓嶇偣璁剧疆涓哄崐寰勭偣锛�");
-            circleGuidancePrimaryButton.setText("鏄�");
-            circleGuidanceSecondaryButton.setVisible(false);
+            circleGuidanceLabel.setText("閲囬泦绗�2涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�2鐐�");
+            circleGuidanceSecondaryButton.setText("杩斿洖");
+            circleGuidanceSecondaryButton.setVisible(true);
+        } else if (step == 3) {
+            circleGuidanceLabel.setText("閲囬泦绗�3涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�3鐐�");
+            circleGuidanceSecondaryButton.setText("杩斿洖");
+            circleGuidanceSecondaryButton.setVisible(true);
+        } else if (step == 4) {
+            circleGuidanceLabel.setText("宸查噰闆嗕笁涓偣");
+            circleGuidancePrimaryButton.setText("缁撴潫缁樺埗");
+            circleGuidanceSecondaryButton.setText("閲嶆柊閲囬泦");
+            circleGuidanceSecondaryButton.setVisible(true);
         } else {
             hideCircleGuidancePanel();
             return;
         }
         circleGuidancePanel.setVisible(true);
 
+        refreshCircleGuidanceButtonAvailability();
+
         if (circleDialogMode) {
             ensureCircleGuidanceDialog();
             if (circleGuidanceDialog != null) {
@@ -933,6 +1090,9 @@
         button.addMouseListener(new MouseAdapter() {
             @Override
             public void mouseEntered(MouseEvent e) {
+                if (!button.isEnabled()) {
+                    return;
+                }
                 if (filled) {
                     button.setBackground(THEME_HOVER_COLOR);
                 } else {
@@ -942,6 +1102,9 @@
 
             @Override
             public void mouseExited(MouseEvent e) {
+                if (!button.isEnabled()) {
+                    return;
+                }
                 if (filled) {
                     button.setBackground(bg);
                 } else {
@@ -953,20 +1116,25 @@
     }
 
     private void onCircleGuidancePrimary() {
-        if (circleGuidanceStep == 1) {
-            showCircleGuidanceStep(2);
-        } else {
+        if (circleGuidanceStep >= 1 && circleGuidanceStep <= 3) {
+            if (!recordCircleSamplePoint()) {
+                return;
+            }
+            if (circleGuidanceStep == 3) {
+                showCircleGuidanceStep(4);
+            } else {
+                showCircleGuidanceStep(circleGuidanceStep + 1);
+            }
+        } else if (circleGuidanceStep == 4) {
             handleCircleCompletion();
         }
     }
 
     private void onCircleGuidanceSecondary() {
-        if (circleGuidanceStep == 1) {
+        if (circleGuidanceStep == 1 || circleGuidanceStep == 2 || circleGuidanceStep == 3) {
             handleCircleAbort(null);
-        } else if (circleGuidanceStep == 2) {
-            handleCircleAbort(null);
-        } else {
-            hideCircleGuidancePanel();
+        } else if (circleGuidanceStep == 4) {
+            restartCircleCapture();
         }
     }
 
@@ -991,6 +1159,7 @@
 
     private void handleCircleCompletion() {
         hideCircleGuidancePanel();
+        clearCircleGuidanceArtifacts();
         if (endDrawingCallback != null) {
             endDrawingCallback.run();
         } else {
@@ -998,8 +1167,295 @@
         }
     }
 
+    private void prepareCircleGuidanceState() {
+        clearCircleGuidanceArtifacts();
+        circleBaseLatLon = resolveCircleBaseLatLon();
+        startCircleDataMonitor();
+    }
+
+    private void clearCircleGuidanceArtifacts() {
+        stopCircleDataMonitor();
+        circleCapturedPoints.clear();
+        circleBaseLatLon = null;
+        lastCapturedCoordinate = null;
+        if (mapRenderer != null) {
+            mapRenderer.clearCircleCaptureOverlay();
+            mapRenderer.clearCircleSampleMarkers();
+        }
+    }
+
+    private void restartCircleCapture() {
+        clearCircleGuidanceArtifacts();
+        synchronized (Coordinate.coordinates) {
+            Coordinate.coordinates.clear();
+        }
+        Coordinate.setStartSaveGngga(true);
+        circleBaseLatLon = resolveCircleBaseLatLon();
+        showCircleGuidanceStep(1);
+        startCircleDataMonitor();
+    }
+
+    private boolean recordCircleSamplePoint() {
+        Coordinate latest = getLatestCoordinate();
+        if (latest == null) {
+            JOptionPane.showMessageDialog(this, "鏈幏鍙栧埌褰撳墠浣嶇疆鍧愭爣锛岃绋嶅悗閲嶈瘯銆�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double[] base = ensureCircleBaseLatLon();
+        if (base == null) {
+            JOptionPane.showMessageDialog(this, "鍩哄噯绔欏潗鏍囨棤鏁堬紝璇峰厛鍦ㄥ熀鍑嗙珯绠$悊涓畬鎴愯缃��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection());
+        double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection());
+        if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
+            JOptionPane.showMessageDialog(this, "閲囬泦鐐瑰潗鏍囨棤鏁堬紝璇烽噸鏂伴噰闆嗐��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]);
+        circleCapturedPoints.add(local);
+        if (mapRenderer != null) {
+            mapRenderer.updateCircleSampleMarkers(circleCapturedPoints);
+        }
+        lastCapturedCoordinate = latest;
+        refreshCircleGuidanceButtonAvailability();
+
+        if (circleCapturedPoints.size() >= 3) {
+            CircleSolution solution = fitCircleFromPoints(circleCapturedPoints);
+            if (solution == null) {
+                circleCapturedPoints.remove(circleCapturedPoints.size() - 1);
+                JOptionPane.showMessageDialog(this, "鏃犳硶鏍规嵁褰撳墠涓変釜鐐圭敓鎴愬渾锛岃閲嶆柊閲囬泦鐐广��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                restartCircleCapture();
+                return false;
+            }
+            if (mapRenderer != null) {
+                mapRenderer.showCircleCaptureOverlay(solution.centerX, solution.centerY, solution.radius, circleCapturedPoints);
+            }
+        }
+
+        return true;
+    }
+
+    private void startCircleDataMonitor() {
+        if (circleDataMonitor == null) {
+            circleDataMonitor = new Timer(600, e -> refreshCircleGuidanceButtonAvailability());
+            circleDataMonitor.setRepeats(true);
+        }
+        if (!circleDataMonitor.isRunning()) {
+            circleDataMonitor.start();
+        }
+        refreshCircleGuidanceButtonAvailability();
+    }
+
+    private void stopCircleDataMonitor() {
+        if (circleDataMonitor != null) {
+            circleDataMonitor.stop();
+        }
+    }
+
+    private void refreshCircleGuidanceButtonAvailability() {
+        if (circleGuidancePrimaryButton == null) {
+            return;
+        }
+        boolean shouldEnable = circleGuidanceStep >= 1 && circleGuidanceStep <= 3
+                ? isCircleDataAvailable()
+                : true;
+        applyCirclePrimaryButtonState(shouldEnable);
+    }
+
+    private boolean isCircleDataAvailable() {
+        Coordinate latest = getLatestCoordinate();
+        if (latest == null) {
+            return false;
+        }
+        return lastCapturedCoordinate == null || latest != lastCapturedCoordinate;
+    }
+
+    private void applyCirclePrimaryButtonState(boolean enabled) {
+        if (circleGuidancePrimaryButton == null) {
+            return;
+        }
+        if (circleGuidanceStep >= 4) {
+            enabled = true;
+        }
+        circleGuidancePrimaryButton.setEnabled(enabled);
+        if (enabled) {
+            circleGuidancePrimaryButton.setBackground(THEME_COLOR);
+            circleGuidancePrimaryButton.setForeground(Color.WHITE);
+            circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        } else {
+            circleGuidancePrimaryButton.setBackground(new Color(200, 200, 200));
+            circleGuidancePrimaryButton.setForeground(new Color(120, 120, 120));
+            circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+        }
+    }
+
+    private Coordinate getLatestCoordinate() {
+        synchronized (Coordinate.coordinates) {
+            int size = Coordinate.coordinates.size();
+            if (size == 0) {
+                return null;
+            }
+            return Coordinate.coordinates.get(size - 1);
+        }
+    }
+
+    private double[] ensureCircleBaseLatLon() {
+        if (circleBaseLatLon == null) {
+            circleBaseLatLon = resolveCircleBaseLatLon();
+        }
+        return circleBaseLatLon;
+    }
+
+    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 (!isMeaningfulValue(coords)) {
+            coords = addzhangaiwu.getActiveSessionBaseStation();
+        }
+        if (!isMeaningfulValue(coords) && baseStation != null) {
+            coords = baseStation.getInstallationCoordinates();
+        }
+        if (!isMeaningfulValue(coords)) {
+            return null;
+        }
+        String[] parts = coords.split(",");
+        if (parts.length < 4) {
+            return null;
+        }
+        double baseLat = parseDMToDecimal(parts[0], parts[1]);
+        double baseLon = parseDMToDecimal(parts[2], parts[3]);
+        if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) {
+            return null;
+        }
+        return new double[]{baseLat, baseLon};
+    }
+
+    private double parseDMToDecimal(String dmm, String direction) {
+        if (dmm == null || dmm.trim().isEmpty()) {
+            return Double.NaN;
+        }
+        try {
+            String trimmed = dmm.trim();
+            int dotIndex = trimmed.indexOf('.');
+            if (dotIndex < 2) {
+                return Double.NaN;
+            }
+            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
+            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
+            double decimal = degrees + minutes / 60.0;
+            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
+                decimal = -decimal;
+            }
+            return decimal;
+        } catch (NumberFormatException ex) {
+            return Double.NaN;
+        }
+    }
+
+    private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
+        double deltaLat = lat - baseLat;
+        double deltaLon = lon - baseLon;
+        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
+        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
+        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
+        return new double[]{eastMeters, northMeters};
+    }
+
+    private CircleSolution fitCircleFromPoints(List<double[]> points) {
+        if (points == null || points.size() < 3) {
+            return null;
+        }
+        CircleSolution best = null;
+        double bestScore = 0.0;
+        int n = points.size();
+        for (int i = 0; i < n - 2; i++) {
+            double[] p1 = points.get(i);
+            for (int j = i + 1; j < n - 1; j++) {
+                double[] p2 = points.get(j);
+                for (int k = j + 1; k < n; k++) {
+                    double[] p3 = points.get(k);
+                    CircleSolution candidate = circleFromThreePoints(p1, p2, p3);
+                    if (candidate == null || candidate.radius <= 0) {
+                        continue;
+                    }
+                    double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3)));
+                    if (minEdge > bestScore) {
+                        bestScore = minEdge;
+                        best = candidate;
+                    }
+                }
+            }
+        }
+        return best;
+    }
+
+    private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) {
+        if (p1 == null || p2 == null || p3 == null) {
+            return null;
+        }
+        double x1 = p1[0];
+        double y1 = p1[1];
+        double x2 = p2[0];
+        double y2 = p2[1];
+        double x3 = p3[0];
+        double y3 = p3[1];
+
+        double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
+        double d = 2.0 * a;
+        if (Math.abs(d) < 1e-6) {
+            return null;
+        }
+
+        double x1Sq = x1 * x1 + y1 * y1;
+        double x2Sq = x2 * x2 + y2 * y2;
+        double x3Sq = x3 * x3 + y3 * y3;
+
+        double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d;
+        double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d;
+        double radius = Math.hypot(centerX - x1, centerY - y1);
+        if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) {
+            return null;
+        }
+        if (radius < 0.05) {
+            return null;
+        }
+        return new CircleSolution(centerX, centerY, radius);
+    }
+
+    private double distance(double[] a, double[] b) {
+        if (a == null || b == null) {
+            return 0.0;
+        }
+        double dx = a[0] - b[0];
+        double dy = a[1] - b[1];
+        return Math.hypot(dx, dy);
+    }
+
+    private static final class CircleSolution {
+        final double centerX;
+        final double centerY;
+        final double radius;
+
+        CircleSolution(double centerX, double centerY, double radius) {
+            this.centerX = centerX;
+            this.centerY = centerY;
+            this.radius = radius;
+        }
+    }
+
     public void hideEndDrawingButton() {
         hideCircleGuidancePanel();
+        clearCircleGuidanceArtifacts();
         hideFloatingDrawingControls();
         circleDialogMode = false;
         applyDrawingPauseState(false, false);

--
Gitblit v1.10.0