From 772144348463f18b958d6e9aa63c43026bf8fee0 Mon Sep 17 00:00:00 2001 From: Yzzz <39290771+lixiaobaivv@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:59:50 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B7=A8=E5=A2=83?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E5=90=8E=E7=9A=84=20MapKit=20=E5=9D=90?= =?UTF-8?q?=E6=A0=87=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/MapHomeView.swift | 69 ++++++++++++++++++- Shared/CoordinateConverter.swift | 68 ++++++++++++++++-- .../FavoriteLocationStoreTests.swift | 37 ++++++++++ Tests/map_refactor_contract_test.sh | 6 ++ 4 files changed, 174 insertions(+), 6 deletions(-) diff --git a/App/MapHomeView.swift b/App/MapHomeView.swift index aa0d1ee..ba70153 100644 --- a/App/MapHomeView.swift +++ b/App/MapHomeView.swift @@ -46,6 +46,7 @@ private enum RealtimeCoordinateSource: Equatable { } struct MapHomeView: View { + @Environment(\.scenePhase) private var scenePhase @ObservedObject var setup: SetupCoordinator @StateObject private var favorites: FavoriteLocationStore @StateObject private var actions = LocationActionCoordinator() @@ -87,6 +88,10 @@ struct MapHomeView: View { @State private var activeSpoofLon: Double? @State private var lastSpoofDiagnosisSystem: CoordinateConverter.MapCoordinateSystem? @State private var hasLoggedSpoofDiagnosis = false + @State private var coordinateSystemRefreshTask: Task? + @State private var coordinateSystemRefreshTaskID: UUID? + @State private var coordinateSystemRefreshAttempts = 0 + @State private var lastCoordinateSystemRefreshAt: Date? init(setup: SetupCoordinator) { self.setup = setup @@ -279,11 +284,19 @@ struct MapHomeView: View { wifiVerificationTask?.cancel() wifiVerificationTask = nil wifiVerificationID = nil + resetMapCoordinateSystemRefresh() + } + .onChange(of: scenePhase) { phase in + guard phase == .active else { return } + resetMapCoordinateSystemRefresh() + refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "App回到前台") } .onChange(of: proxy.isRunning) { running in if runtimeMode.mode == .localWiFi, !running && spoofState == .active { spoofState = .idle actions.clear() + resetMapCoordinateSystemRefresh() + refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "本机代理意外停止") } } .onChange(of: runtimeMode.mode) { mode in @@ -296,6 +309,7 @@ struct MapHomeView: View { } wifiVerificationTask?.cancel() wifiVerificationTask = nil + resetMapCoordinateSystemRefresh() activeSpoofLat = nil activeSpoofLon = nil if mode == .localWiFi { @@ -602,6 +616,7 @@ struct MapHomeView: View { spoofState = .active activeSpoofLat = response.latitude activeSpoofLon = response.longitude + resetMapCoordinateSystemRefresh() RuntimeLogger.info("APP", "定位", "第三方代理坐标同步成功", details: [ "坐标标准": "WGS-84", "客户端模式": "测试模式", @@ -640,6 +655,7 @@ struct MapHomeView: View { activeSpoofLon = target.longitude lastSpoofDiagnosisSystem = nil hasLoggedSpoofDiagnosis = false + resetMapCoordinateSystemRefresh() } RuntimeLogger.info("APP", "定位", "验证结果", details: [ "success": "true", @@ -679,6 +695,8 @@ struct MapHomeView: View { spoofState = .idle activeSpoofLat = nil activeSpoofLon = nil + resetMapCoordinateSystemRefresh() + refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "第三方虚拟定位已停用") presentSuccessfulOperationTip(.deactivation) } catch { spoofState = .active @@ -695,6 +713,8 @@ struct MapHomeView: View { activeSpoofLon = nil lastSpoofDiagnosisSystem = nil hasLoggedSpoofDiagnosis = false + resetMapCoordinateSystemRefresh() + refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "APP虚拟定位已停用") presentSuccessfulOperationTip(.deactivation) } @@ -985,12 +1005,17 @@ struct MapHomeView: View { lon: longitude, mapCoordinateSystem: .wgs84 ) + let maximumTargetDistance = max(1_000, location.horizontalAccuracy * 4) let diagnosis = CoordinateConverter.diagnoseRepresentation( sample: location.coordinate, pair: targetPair, - maximumDistance: max(1_000, location.horizontalAccuracy * 4), + maximumDistance: maximumTargetDistance, minimumSeparation: max(30, location.horizontalAccuracy) ) + let nearestTargetDistance = min(diagnosis.distanceToWGS84, diagnosis.distanceToGCJ02) + if nearestTargetDistance <= maximumTargetDistance { + refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "虚拟定位蓝点已到达目标") + } let shouldLog = !hasLoggedSpoofDiagnosis || diagnosis.inferredSystem != lastSpoofDiagnosisSystem guard shouldLog else { return } @@ -1002,10 +1027,52 @@ struct MapHomeView: View { "蓝点回调更接近": diagnosis.inferredName, "蓝点距WGS目标米": String(format: "%.1f", diagnosis.distanceToWGS84), "蓝点距GCJ目标米": String(format: "%.1f", diagnosis.distanceToGCJ02), + "蓝点已到达目标": String(nearestTargetDistance <= maximumTargetDistance), "日志策略": "每次开启首次或判定变化" ]) } + private func resetMapCoordinateSystemRefresh() { + coordinateSystemRefreshTask?.cancel() + coordinateSystemRefreshTask = nil + coordinateSystemRefreshTaskID = nil + coordinateSystemRefreshAttempts = 0 + lastCoordinateSystemRefreshAt = nil + } + + private func refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: String) { + guard coordinateSystemRefreshTask == nil, + coordinateSystemRefreshAttempts < 3 else { return } + if let lastCoordinateSystemRefreshAt, + Date().timeIntervalSince(lastCoordinateSystemRefreshAt) < 2 { + return + } + coordinateSystemRefreshAttempts += 1 + lastCoordinateSystemRefreshAt = Date() + let attempt = coordinateSystemRefreshAttempts + let taskID = UUID() + coordinateSystemRefreshTaskID = taskID + coordinateSystemRefreshTask = Task { @MainActor in + defer { + if coordinateSystemRefreshTaskID == taskID { + coordinateSystemRefreshTask = nil + coordinateSystemRefreshTaskID = nil + } + } + let change = await CoordinateConverter.refreshMapCoordinateSystem() + guard !Task.isCancelled, coordinateSystemRefreshTaskID == taskID else { return } + if let change { + reprojectMapSelection(for: change) + } + RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新请求结束", details: [ + "触发": trigger, + "尝试": String(attempt), + "发生切换": String(change != nil), + "当前标准": CoordinateConverter.currentMapCoordinateSystem.rawValue + ]) + } + } + private func startRealtimeLocationRequest( source: String, showFailureAlert: Bool, diff --git a/Shared/CoordinateConverter.swift b/Shared/CoordinateConverter.swift index a475a5e..5cec59e 100644 --- a/Shared/CoordinateConverter.swift +++ b/Shared/CoordinateConverter.swift @@ -110,7 +110,7 @@ enum CoordinateConverter { let nextType: MapCoordinateSystem switch await fixedAnchorCoordinateSystemProbe() { case let .response(name, count): - nextType = name == "林士街" ? .gcj02 : .wgs84 + nextType = mapCoordinateSystem(anchorName: name) initialMapCoordinateSystemUsedFallback = false RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测获得明确结果", details: [ "首条名称": name, @@ -146,7 +146,7 @@ enum CoordinateConverter { return currentMapCoordinateSystem } - currentMapCoordinateSystem = nextType + _ = applyDetectedMapCoordinateSystem(nextType) RuntimeLogger.info("APP", "坐标转换", "地图坐标标准已确定,允许创建地图", details: [ "最终标准": nextType.rawValue, "使用兜底": String(initialMapCoordinateSystemUsedFallback), @@ -155,6 +155,66 @@ enum CoordinateConverter { return nextType } + /// Re-runs the same location-independent fixed-anchor probe after a + /// spoofing environment change. Runtime callers must never infer MapKit's + /// representation from the geographic region of the current location: that + /// sample may itself already be virtual. + @MainActor + static func refreshMapCoordinateSystem() async -> MapCoordinateSystemChange? { + guard !mapCoordinateSystemCheckPending else { + RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新已在进行中") + return nil + } + mapCoordinateSystemCheckPending = true + defer { mapCoordinateSystemCheckPending = false } + + RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新开始", details: [ + "当前标准": currentMapCoordinateSystem.rawValue, + "锚点": "22.283819,114.158439" + ]) + switch await fixedAnchorCoordinateSystemProbe() { + case let .response(name, count): + let detected = mapCoordinateSystem(anchorName: name) + initialMapCoordinateSystemUsedFallback = false + let change = applyDetectedMapCoordinateSystem(detected) + RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新完成", details: [ + "首条名称": name, + "结果数": String(count), + "探测标准": detected.rawValue, + "发生切换": String(change != nil) + ]) + return change + case .unavailable(let reason), .timedOut(let reason): + RuntimeLogger.warning("APP", "坐标转换", "运行期地图坐标标准刷新不可用,保留当前标准", details: [ + "原因": reason, + "当前标准": currentMapCoordinateSystem.rawValue + ]) + return nil + case .cancelled: + RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新被取消,保留当前标准", details: [ + "当前标准": currentMapCoordinateSystem.rawValue + ]) + return nil + } + } + + /// Applies a coordinate system that was resolved by the fixed-anchor + /// probe. Kept as a small deterministic seam for regression tests. + @MainActor + static func applyDetectedMapCoordinateSystem(_ detected: MapCoordinateSystem) -> MapCoordinateSystemChange? { + guard detected != currentMapCoordinateSystem else { return nil } + let change = MapCoordinateSystemChange( + previous: currentMapCoordinateSystem, + current: detected + ) + currentMapCoordinateSystem = detected + return change + } + + private static func mapCoordinateSystem(anchorName: String) -> MapCoordinateSystem { + anchorName == "林士街" ? .gcj02 : .wgs84 + } + /// A user-requested realtime sample is WGS-84 and can correct a provisional /// startup map coordinate system without altering persisted coordinate pairs. @MainActor @@ -167,14 +227,12 @@ enum CoordinateConverter { return nil } let next: MapCoordinateSystem = usesGCJ02ServiceArea(lat: coordinate.latitude, lon: coordinate.longitude) ? .gcj02 : .wgs84 - guard next != currentMapCoordinateSystem else { + guard let change = applyDetectedMapCoordinateSystem(next) else { RuntimeLogger.info("APP", "坐标转换", "实时定位确认兜底地图坐标标准无需修正", details: [ "当前标准": currentMapCoordinateSystem.rawValue ]) return nil } - let change = MapCoordinateSystemChange(previous: currentMapCoordinateSystem, current: next) - currentMapCoordinateSystem = next RuntimeLogger.warning("APP", "坐标转换", "实时定位修正启动兜底地图坐标标准", details: [ "from": change.previous.rawValue, "to": change.current.rawValue diff --git a/Tests/PaopaoLocationSpooferTests/FavoriteLocationStoreTests.swift b/Tests/PaopaoLocationSpooferTests/FavoriteLocationStoreTests.swift index d687209..c569159 100644 --- a/Tests/PaopaoLocationSpooferTests/FavoriteLocationStoreTests.swift +++ b/Tests/PaopaoLocationSpooferTests/FavoriteLocationStoreTests.swift @@ -122,6 +122,43 @@ final class FavoriteLocationStoreTests: XCTestCase { XCTAssertNil(CoordinateConverter.diagnoseRepresentation(sample: unrelated, pair: pair).inferredSystem) } + @MainActor + func testDetectedMapCoordinateSystemChangePreventsHongKongWGS84DoubleConversion() { + let original = CoordinateConverter.currentMapCoordinateSystem + defer { _ = CoordinateConverter.applyDetectedMapCoordinateSystem(original) } + _ = CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02) + + let hongKongObservatory = CLLocationCoordinate2D( + latitude: 22.302_344, + longitude: 114.174_566 + ) + let stalePair = CoordinatePair( + mapCoordinate: hongKongObservatory, + mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem + ) + + let change = CoordinateConverter.applyDetectedMapCoordinateSystem(.wgs84) + let pair = CoordinatePair( + mapCoordinate: hongKongObservatory, + mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem + ) + + XCTAssertEqual(change?.previous, .gcj02) + XCTAssertEqual(change?.current, .wgs84) + XCTAssertNotEqual(stalePair.wgs84.longitude, hongKongObservatory.longitude) + XCTAssertEqual(pair.wgs84.latitude, hongKongObservatory.latitude, accuracy: 0.000_000_1) + XCTAssertEqual(pair.wgs84.longitude, hongKongObservatory.longitude, accuracy: 0.000_000_1) + } + + @MainActor + func testApplyingSameDetectedMapCoordinateSystemDoesNotReportAChange() { + let original = CoordinateConverter.currentMapCoordinateSystem + defer { _ = CoordinateConverter.applyDetectedMapCoordinateSystem(original) } + _ = CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02) + + XCTAssertNil(CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02)) + } + func testMapConfigurationNeverRequestsRealUserLocation() { XCTAssertFalse(MapConfiguration.default.showsUserLocation) XCTAssertFalse(MapConfiguration.default.allowsCurrentLocationRequest) diff --git a/Tests/map_refactor_contract_test.sh b/Tests/map_refactor_contract_test.sh index 1d3ed03..e88098a 100755 --- a/Tests/map_refactor_contract_test.sh +++ b/Tests/map_refactor_contract_test.sh @@ -65,6 +65,12 @@ if grep -q 'logEvent("CONNECT " + host + " -> passthrough")' "$ROOT/Core/proxy.g fi grep -q 'enum SystemSettingsNavigator' "$SETTINGS_NAVIGATOR" || fail "shared settings navigator is missing" grep -q 'await CoordinateConverter.resolveInitialMapCoordinateSystem()' "$CONTENT" || fail "map type must resolve before MapHomeView construction" +grep -q 'static func refreshMapCoordinateSystem() async' "$CONVERTER" || fail "runtime MapKit coordinate-system changes must reuse the fixed-anchor probe" +grep -q 'fixedAnchorCoordinateSystemProbe()' "$CONVERTER" || fail "runtime coordinate-system refresh must use the location-independent fixed anchor" +test "$(grep -c 'initialMapCoordinateSystemUsedFallback = false' "$CONVERTER")" -ge 2 || fail "a successful runtime anchor probe must disable location-region fallback correction" +grep -q 'refreshMapCoordinateSystemAfterLocationEnvironmentChange' "$MAP_HOME" || fail "MapHomeView must refresh the coordinate system after spoofing changes the location environment" +grep -q 'nearestTargetDistance' "$MAP_HOME" || fail "spoof refresh must wait until the MapKit blue point reaches the active target" +grep -q 'reprojectMapSelection(for: change)' "$MAP_HOME" || fail "coordinate-system refresh must replay the persisted coordinate pair" grep -q 'phase = .map' "$CONTENT" || fail "ContentView must explicitly gate MapHomeView construction" ! grep -q 'startTileProbe' "$MAP_HOME" || fail "MapHomeView must not start a second fixed-anchor coordinate-system probe" ! grep -q 'initializeMap()' "$MAP_HOME" || fail "MapHomeView must not replay a second map initialization from onAppear"