mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
fix: 修复跨境定位后的 MapKit 坐标偏移
This commit is contained in:
+68
-1
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user