mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
fix: 优化启动引导、坐标处理和日志管理
This commit is contained in:
+180
-121
@@ -2,121 +2,199 @@ import Foundation
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
struct CoordinatePair: Codable, Equatable {
|
||||
static let currentConversionVersion = 1
|
||||
|
||||
struct Value: Codable, Equatable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
|
||||
var coordinate: CLLocationCoordinate2D {
|
||||
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
}
|
||||
}
|
||||
|
||||
let wgs84: Value
|
||||
let gcj02: Value
|
||||
let conversionVersion: Int
|
||||
|
||||
init(wgs84: Value, gcj02: Value, conversionVersion: Int = CoordinatePair.currentConversionVersion) {
|
||||
self.wgs84 = wgs84
|
||||
self.gcj02 = gcj02
|
||||
self.conversionVersion = conversionVersion
|
||||
}
|
||||
|
||||
init(mapCoordinate: CLLocationCoordinate2D, mapCoordinateSystem: CoordinateConverter.MapCoordinateSystem) {
|
||||
self = CoordinateConverter.coordinatePair(
|
||||
lat: mapCoordinate.latitude,
|
||||
lon: mapCoordinate.longitude,
|
||||
mapCoordinateSystem: mapCoordinateSystem
|
||||
)
|
||||
}
|
||||
|
||||
func coordinate(for mapCoordinateSystem: CoordinateConverter.MapCoordinateSystem) -> CLLocationCoordinate2D {
|
||||
switch mapCoordinateSystem {
|
||||
case .wgs84: return wgs84.coordinate
|
||||
case .gcj02: return gcj02.coordinate
|
||||
}
|
||||
}
|
||||
|
||||
func matchesWGS84(latitude: Double, longitude: Double, tolerance: Double = 0.0001) -> Bool {
|
||||
abs(wgs84.latitude - latitude) <= tolerance
|
||||
&& abs(wgs84.longitude - longitude) <= tolerance
|
||||
}
|
||||
}
|
||||
|
||||
/// GCJ-02 (火星坐标) ↔ WGS-84 坐标转换。
|
||||
///
|
||||
/// MKMapView 根据实时定位动态切换瓦片源:中国境内用高德 GCJ-02,境外用 Apple WGS-84。
|
||||
/// App 内部统一以 WGS-84 存储,仅在地图交互时按当前瓦片类型双向转换。
|
||||
/// MapKit does not expose a public API for its active coordinate reference system.
|
||||
/// The app resolves it with a bounded heuristic, then stores both WGS-84 and
|
||||
/// GCJ-02 representations at each write boundary so replay does not convert again.
|
||||
enum CoordinateConverter {
|
||||
// 椭球参数 (Krasovsky 1940)
|
||||
private static let a = 6378245.0
|
||||
private static let ee = 0.00669342162296594323
|
||||
|
||||
/// 坐标类型
|
||||
enum CoordType: String {
|
||||
enum MapCoordinateSystem: String {
|
||||
case gcj02 = "GCJ-02"
|
||||
case wgs84 = "WGS-84"
|
||||
}
|
||||
|
||||
// MARK: - 全局瓦片类型
|
||||
// MARK: - 地图坐标标准
|
||||
|
||||
struct TileTypeChange: Equatable {
|
||||
let previous: CoordType
|
||||
let current: CoordType
|
||||
struct MapCoordinateSystemChange: Equatable {
|
||||
let previous: MapCoordinateSystem
|
||||
let current: MapCoordinateSystem
|
||||
}
|
||||
|
||||
/// 当前地图瓦片坐标系。探测不可用时使用国内 GCJ-02 作为兜底。
|
||||
@MainActor static var currentTileType = CoordType.gcj02
|
||||
@MainActor private static var lastTileCheck: Date?
|
||||
@MainActor private static var tileCheckPending = false
|
||||
/// 当前 Apple 地图坐标标准。检测不可用时使用国内 GCJ-02 作为兜底。
|
||||
@MainActor static var currentMapCoordinateSystem = MapCoordinateSystem.gcj02
|
||||
@MainActor private static var mapCoordinateSystemCheckPending = false
|
||||
@MainActor private(set) static var initialMapCoordinateSystemUsedFallback = true
|
||||
|
||||
/// Uses one fixed, known reference result as a best-effort tile heuristic.
|
||||
///
|
||||
/// MapKit does not expose a supported public API for its active tile CRS. A
|
||||
/// timeout, empty response, or search error is therefore not evidence for
|
||||
/// WGS-84: those cases deliberately fall back to the domestic GCJ-02 mode.
|
||||
/// Startup gate: only request realtime location if the public MapKit probe
|
||||
/// cannot resolve a coordinate type. This guarantees a finite answer before
|
||||
/// persisted map positions are replayed.
|
||||
@MainActor
|
||||
@discardableResult
|
||||
static func detectTileByFixedGeocode(force: Bool = false) async -> TileTypeChange? {
|
||||
guard !tileCheckPending else {
|
||||
RuntimeLogger.info("APP", "坐标转换", "瓦片检测: 跳过(进行中)")
|
||||
return nil
|
||||
static func resolveInitialMapCoordinateSystem() async -> MapCoordinateSystem {
|
||||
guard !mapCoordinateSystemCheckPending else {
|
||||
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准检测已有请求进行中")
|
||||
return currentMapCoordinateSystem
|
||||
}
|
||||
if !force, let last = lastTileCheck, -last.timeIntervalSinceNow < 30 {
|
||||
RuntimeLogger.info("APP", "坐标转换", "瓦片检测: 跳过(缓存\(Int(-last.timeIntervalSinceNow))s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
tileCheckPending = true
|
||||
defer { tileCheckPending = false }
|
||||
RuntimeLogger.info("APP", "坐标转换", "瓦片检测: 发起查询", details: [
|
||||
"force": String(force),
|
||||
"当前瓦片": currentTileType.rawValue
|
||||
mapCoordinateSystemCheckPending = true
|
||||
defer { mapCoordinateSystemCheckPending = false }
|
||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测开始", details: [
|
||||
"锚点": "22.283819,114.158439",
|
||||
"判定规则": "首条名称=林士街→GCJ-02,否则→WGS-84",
|
||||
"缓存": "false"
|
||||
])
|
||||
|
||||
let probeResult = await fixedGeocodeProbe()
|
||||
guard !Task.isCancelled else { return nil }
|
||||
lastTileCheck = Date()
|
||||
|
||||
let nextType: CoordType
|
||||
switch probeResult {
|
||||
let nextType: MapCoordinateSystem
|
||||
switch await fixedAnchorCoordinateSystemProbe() {
|
||||
case let .response(name, count):
|
||||
nextType = name == "林士街" ? .gcj02 : .wgs84
|
||||
RuntimeLogger.info("APP", "坐标转换", "瓦片检测完成 → \(nextType.rawValue)", details: [
|
||||
initialMapCoordinateSystemUsedFallback = false
|
||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测获得明确结果", details: [
|
||||
"首条名称": name,
|
||||
"结果数": String(count),
|
||||
"命中锚点": String(nextType == .gcj02)
|
||||
"命中林士街": String(name == "林士街"),
|
||||
"最终标准": nextType.rawValue,
|
||||
"结果来源": "固定锚点"
|
||||
])
|
||||
case .unavailable(let reason), .timedOut(let reason):
|
||||
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准检测不可用,开始实时定位兜底", details: [
|
||||
"原因": reason
|
||||
])
|
||||
let realtime = await RealtimeLocationManager.shared.requestLocation()
|
||||
if let realtime,
|
||||
CLLocationCoordinate2DIsValid(realtime),
|
||||
!usesGCJ02ServiceArea(lat: realtime.latitude, lon: realtime.longitude) {
|
||||
nextType = .wgs84
|
||||
} else {
|
||||
nextType = .gcj02
|
||||
}
|
||||
initialMapCoordinateSystemUsedFallback = true
|
||||
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准检测使用兜底结果", details: [
|
||||
"探测失败原因": reason,
|
||||
"实时定位存在": String(realtime != nil),
|
||||
"最终标准": nextType.rawValue,
|
||||
"结果来源": realtime == nil ? "默认国内标准" : "实时定位服务区域"
|
||||
])
|
||||
case .unavailable:
|
||||
nextType = .gcj02
|
||||
RuntimeLogger.warning("APP", "坐标转换", "瓦片检测无结果,回退 GCJ-02")
|
||||
case .timedOut:
|
||||
nextType = .gcj02
|
||||
RuntimeLogger.warning("APP", "坐标转换", "瓦片检测超时,回退 GCJ-02")
|
||||
case .cancelled:
|
||||
return nil
|
||||
initialMapCoordinateSystemUsedFallback = true
|
||||
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准检测被取消,保留默认国内标准", details: [
|
||||
"最终标准": currentMapCoordinateSystem.rawValue
|
||||
])
|
||||
return currentMapCoordinateSystem
|
||||
}
|
||||
|
||||
guard nextType != currentTileType else { return nil }
|
||||
let change = TileTypeChange(previous: currentTileType, current: nextType)
|
||||
currentTileType = nextType
|
||||
currentMapCoordinateSystem = nextType
|
||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准已确定,允许创建地图", details: [
|
||||
"最终标准": nextType.rawValue,
|
||||
"使用兜底": String(initialMapCoordinateSystemUsedFallback),
|
||||
"缓存": "false"
|
||||
])
|
||||
return nextType
|
||||
}
|
||||
|
||||
/// A user-requested realtime sample is WGS-84 and can correct a provisional
|
||||
/// startup map coordinate system without altering persisted coordinate pairs.
|
||||
@MainActor
|
||||
static func correctMapCoordinateSystemUsingRealtime(_ coordinate: CLLocationCoordinate2D) -> MapCoordinateSystemChange? {
|
||||
guard CLLocationCoordinate2DIsValid(coordinate) else { return nil }
|
||||
guard initialMapCoordinateSystemUsedFallback else {
|
||||
RuntimeLogger.info("APP", "坐标转换", "实时定位不覆盖固定锚点的明确检测结果", details: [
|
||||
"当前标准": currentMapCoordinateSystem.rawValue
|
||||
])
|
||||
return nil
|
||||
}
|
||||
let next: MapCoordinateSystem = usesGCJ02ServiceArea(lat: coordinate.latitude, lon: coordinate.longitude) ? .gcj02 : .wgs84
|
||||
guard next != currentMapCoordinateSystem 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
|
||||
])
|
||||
return change
|
||||
}
|
||||
|
||||
/// Reprojects a map-display coordinate after the tile heuristic changes.
|
||||
/// The physical coordinate remains WGS-84 in between the two display modes.
|
||||
static func reprojectDisplayCoordinate(
|
||||
_ coordinate: CLLocationCoordinate2D,
|
||||
from previous: CoordType,
|
||||
to current: CoordType
|
||||
) -> CLLocationCoordinate2D {
|
||||
guard previous != current else { return coordinate }
|
||||
let stored = storedCoordinate(lat: coordinate.latitude, lon: coordinate.longitude, tileType: previous)
|
||||
let display = displayCoordinate(lat: stored.lat, lon: stored.lon, tileType: current)
|
||||
return CLLocationCoordinate2D(latitude: display.lat, longitude: display.lon)
|
||||
}
|
||||
|
||||
private static func fixedGeocodeProbe() async -> TileProbeResult {
|
||||
private static func fixedAnchorCoordinateSystemProbe() async -> MapCoordinateSystemProbeResult {
|
||||
let request = MKLocalSearch.Request()
|
||||
request.naturalLanguageQuery = "22.283819, 114.158439"
|
||||
let search = MKLocalSearch(request: request)
|
||||
let resolver = TileProbeResolver()
|
||||
let resolver = MapCoordinateSystemProbeResolver()
|
||||
|
||||
return await withTaskCancellationHandler(operation: {
|
||||
await withCheckedContinuation { continuation in
|
||||
let timeout = DispatchWorkItem {
|
||||
search.cancel()
|
||||
resolver.resolve(.timedOut)
|
||||
resolver.resolve(.timedOut(reason: "固定锚点查询超过5秒"))
|
||||
}
|
||||
resolver.install(continuation, timeout: timeout)
|
||||
guard !resolver.isResolved else { return }
|
||||
search.start { response, error in
|
||||
guard error == nil else {
|
||||
resolver.resolve(.unavailable)
|
||||
if let error {
|
||||
let nsError = error as NSError
|
||||
resolver.resolve(.unavailable(
|
||||
reason: "\(nsError.domain)(\(nsError.code)): \(nsError.localizedDescription)"
|
||||
))
|
||||
return
|
||||
}
|
||||
resolver.resolve(.response(
|
||||
name: response?.mapItems.first?.name ?? "",
|
||||
count: response?.mapItems.count ?? 0
|
||||
))
|
||||
let items = response?.mapItems ?? []
|
||||
guard let first = items.first,
|
||||
let name = first.name?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!name.isEmpty else {
|
||||
resolver.resolve(.unavailable(reason: "固定锚点查询返回空结果"))
|
||||
return
|
||||
}
|
||||
resolver.resolve(.response(name: name, count: items.count))
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: timeout)
|
||||
}
|
||||
@@ -126,48 +204,30 @@ enum CoordinateConverter {
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - 存取转换
|
||||
|
||||
/// 地图坐标 → WGS-84 存储
|
||||
@MainActor
|
||||
static func toStored(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
|
||||
let stored = storedCoordinate(lat: lat, lon: lon, tileType: currentTileType)
|
||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标已规范为 WGS-84", details: [
|
||||
"转换": String(currentTileType == .gcj02 && usesGCJ02ServiceArea(lat: lat, lon: lon))
|
||||
])
|
||||
return stored
|
||||
}
|
||||
|
||||
/// WGS-84 存储 → 当前地图瓦片坐标系(显示用)
|
||||
@MainActor
|
||||
static func toDisplay(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
|
||||
let display = displayCoordinate(lat: lat, lon: lon, tileType: currentTileType)
|
||||
RuntimeLogger.info("APP", "坐标转换", "WGS-84 坐标已适配地图显示", details: [
|
||||
"转换": String(currentTileType == .gcj02 && usesGCJ02ServiceArea(lat: lat, lon: lon))
|
||||
])
|
||||
return display
|
||||
}
|
||||
|
||||
private static func storedCoordinate(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
tileType: CoordType
|
||||
) -> (lat: Double, lon: Double) {
|
||||
guard tileType == .gcj02, usesGCJ02ServiceArea(lat: lat, lon: lon) else {
|
||||
return (lat, lon)
|
||||
/// Creates the complete persisted pair once at the map input boundary.
|
||||
static func coordinatePair(lat: Double, lon: Double, mapCoordinateSystem: MapCoordinateSystem) -> CoordinatePair {
|
||||
let raw = CoordinatePair.Value(latitude: lat, longitude: lon)
|
||||
switch mapCoordinateSystem {
|
||||
case .wgs84:
|
||||
let gcj = wgs84ToGcj02(lat: lat, lon: lon)
|
||||
return CoordinatePair(
|
||||
wgs84: raw,
|
||||
gcj02: .init(latitude: gcj.lat, longitude: gcj.lon)
|
||||
)
|
||||
case .gcj02:
|
||||
let wgs = gcj02ToWgs84(lat: lat, lon: lon)
|
||||
return CoordinatePair(
|
||||
wgs84: .init(latitude: wgs.lat, longitude: wgs.lon),
|
||||
gcj02: raw
|
||||
)
|
||||
}
|
||||
return gcj02ToWgs84(lat: lat, lon: lon)
|
||||
}
|
||||
|
||||
private static func displayCoordinate(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
tileType: CoordType
|
||||
) -> (lat: Double, lon: Double) {
|
||||
guard tileType == .gcj02, usesGCJ02ServiceArea(lat: lat, lon: lon) else {
|
||||
return (lat, lon)
|
||||
}
|
||||
return wgs84ToGcj02(lat: lat, lon: lon)
|
||||
/// Legacy raw domestic map values were historically displayed as GCJ-02;
|
||||
/// overseas values were WGS-84 and stay identity coordinates.
|
||||
static func legacyCoordinatePair(lat: Double, lon: Double) -> CoordinatePair {
|
||||
let type: MapCoordinateSystem = usesGCJ02ServiceArea(lat: lat, lon: lon) ? .gcj02 : .wgs84
|
||||
return coordinatePair(lat: lat, lon: lon, mapCoordinateSystem: type)
|
||||
}
|
||||
|
||||
// MARK: - 工具
|
||||
@@ -205,9 +265,8 @@ enum CoordinateConverter {
|
||||
return (lat + d.lat, lon + d.lon)
|
||||
}
|
||||
|
||||
/// AMap documents GCJ-02 for mainland China, Hong Kong, Macao and Taiwan;
|
||||
/// its overseas world map uses WGS-84. Keep the bounds explicit so the
|
||||
/// domestic fallback tile type never shifts an overseas coordinate.
|
||||
/// Keep the GCJ-02 service region explicit. Outside this region, conversion
|
||||
/// is identity so the domestic fallback can never shift an overseas value.
|
||||
static func usesGCJ02ServiceArea(lat: Double, lon: Double) -> Bool {
|
||||
let mainland = lat >= 0.8293 && lat <= 55.8271 && lon >= 72.004 && lon <= 137.8347
|
||||
let hongKong = lat >= 22.13 && lat <= 22.57 && lon >= 113.82 && lon <= 114.45
|
||||
@@ -249,17 +308,17 @@ enum CoordinateConverter {
|
||||
}
|
||||
}
|
||||
|
||||
private enum TileProbeResult {
|
||||
private enum MapCoordinateSystemProbeResult {
|
||||
case response(name: String, count: Int)
|
||||
case unavailable
|
||||
case timedOut
|
||||
case unavailable(reason: String)
|
||||
case timedOut(reason: String)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
private final class TileProbeResolver: @unchecked Sendable {
|
||||
private final class MapCoordinateSystemProbeResolver: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var result: TileProbeResult?
|
||||
private var continuation: CheckedContinuation<TileProbeResult, Never>?
|
||||
private var result: MapCoordinateSystemProbeResult?
|
||||
private var continuation: CheckedContinuation<MapCoordinateSystemProbeResult, Never>?
|
||||
private var timeout: DispatchWorkItem?
|
||||
|
||||
var isResolved: Bool {
|
||||
@@ -269,7 +328,7 @@ private final class TileProbeResolver: @unchecked Sendable {
|
||||
}
|
||||
|
||||
func install(
|
||||
_ continuation: CheckedContinuation<TileProbeResult, Never>,
|
||||
_ continuation: CheckedContinuation<MapCoordinateSystemProbeResult, Never>,
|
||||
timeout: DispatchWorkItem
|
||||
) {
|
||||
lock.lock()
|
||||
@@ -284,7 +343,7 @@ private final class TileProbeResolver: @unchecked Sendable {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func resolve(_ nextResult: TileProbeResult) {
|
||||
func resolve(_ nextResult: MapCoordinateSystemProbeResult) {
|
||||
lock.lock()
|
||||
guard result == nil else {
|
||||
lock.unlock()
|
||||
|
||||
Reference in New Issue
Block a user