fix: 修复跨境定位后的 MapKit 坐标偏移

This commit is contained in:
Yzzz
2026-08-07 13:59:50 +08:00
parent 7e362e6b6c
commit 7721443484
4 changed files with 174 additions and 6 deletions
+68 -1
View File
@@ -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<Void, Never>?
@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,
+63 -5
View File
@@ -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
@@ -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)
+6
View File
@@ -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"