Files
location-spoofer/Shared/CoordinateConverter.swift
T

160 lines
6.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import CoreLocation
import MapKit
/// GCJ-02 (火星坐标) ↔ WGS-84 坐标转换。
///
/// MKMapView 根据实时定位动态切换瓦片源:中国境内用高德 GCJ-02,境外用 Apple WGS-84。
/// App 内部统一以 WGS-84 存储,仅在地图交互时按当前瓦片类型双向转换。
enum CoordinateConverter {
// 椭球参数 (Krasovsky 1940)
private static let a = 6378245.0
private static let ee = 0.00669342162296594323
/// 坐标类型
enum CoordType: String {
case gcj02 = "GCJ-02"
case wgs84 = "WGS-84"
}
// MARK: - 全局瓦片类型
/// 当前地图瓦片坐标系
@MainActor static var currentTileType = CoordType.gcj02
/// 瓦片检测缓存
@MainActor private static var lastTileCheck: Date?
@MainActor private static var tileCheckPending = false
/// 固定坐标反查:返回"林士街"=GCJ-02高德瓦片,否则=WGS-8430s缓存)
@MainActor
static func detectTileByFixedGeocode() {
guard !tileCheckPending else { return }
if let last = lastTileCheck, -last.timeIntervalSinceNow < 30 { return }
tileCheckPending = true
let loc = CLLocation(latitude: 22.283819, longitude: 114.158439)
let req = MKLocalSearch.Request()
req.region = MKCoordinateRegion(center: loc.coordinate, latitudinalMeters: 200, longitudinalMeters: 200)
MKLocalSearch(request: req).start { response, _ in
Task { @MainActor in
defer { tileCheckPending = false }
let name = response?.mapItems.first?.name ?? ""
let newType: CoordType = (name == "林士街") ? .gcj02 : .wgs84
RuntimeLogger.info("APP", "坐标转换", "固定坐标反查 → \(newType.rawValue)", details: [
"名称": name
])
if newType != currentTileType {
currentTileType = newType
}
lastTileCheck = Date()
}
}
}
// MARK: - 存取转换
/// 地图坐标 → WGS-84 存储
@MainActor
static func toStored(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
detectTileByFixedGeocode()
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toStored: 不转 瓦片=\(currentTileType.rawValue)", details: [
"lat": String(lat), "lon": String(lon)
])
return (lat, lon)
}
let wgs = gcj02ToWgs84(lat: lat, lon: lon)
let d = distance(lat1: lat, lon1: lon, lat2: wgs.lat, lon2: wgs.lon)
RuntimeLogger.info("APP", "坐标转换", "toStored: GCJ-02 → WGS-84 瓦片=\(currentTileType.rawValue)", details: [
"原始": "\(lat), \(lon)",
"结果": "\(wgs.lat), \(wgs.lon)",
"偏移": String(format: "%.0fm", d)
])
return wgs
}
/// WGS-84 存储 → 当前地图瓦片坐标系(显示用)
@MainActor
static func toDisplay(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
detectTileByFixedGeocode()
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toDisplay: WGS-84 → 不转 瓦片=\(currentTileType.rawValue)", details: [
"lat": String(lat), "lon": String(lon)
])
return (lat, lon)
}
let gcj = wgs84ToGcj02(lat: lat, lon: lon)
let d = distance(lat1: lat, lon1: lon, lat2: gcj.lat, lon2: gcj.lon)
RuntimeLogger.info("APP", "坐标转换", "toDisplay: WGS-84 → GCJ-02 瓦片=\(currentTileType.rawValue)", details: [
"原始": "\(lat), \(lon)",
"结果": "\(gcj.lat), \(gcj.lon)",
"偏移": String(format: "%.0fm", d)
])
return gcj
}
// MARK: - 工具
/// Haversine 距离(米)
static func distance(lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> Double {
let r = 6371000.0
let dLat = (lat2 - lat1) * .pi / 180.0
let dLon = (lon2 - lon1) * .pi / 180.0
let a = sin(dLat / 2) * sin(dLat / 2)
+ cos(lat1 * .pi / 180.0) * cos(lat2 * .pi / 180.0)
* sin(dLon / 2) * sin(dLon / 2)
return r * 2 * atan2(sqrt(a), sqrt(1 - a))
}
// MARK: - 核心转换
/// GCJ-02 → WGS-84(迭代法,精度优于 0.5 米)
static func gcj02ToWgs84(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
var wgsLat = lat
var wgsLon = lon
for _ in 0..<2 {
let d = delta(lat: wgsLat, lon: wgsLon)
wgsLat = lat - d.lat
wgsLon = lon - d.lon
}
return (wgsLat, wgsLon)
}
/// WGS-84 → GCJ-02
static func wgs84ToGcj02(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
let d = delta(lat: lat, lon: lon)
return (lat + d.lat, lon + d.lon)
}
// MARK: - 内部
/// 计算偏移量 (WGS-84 → GCJ-02 的增量)
private static func delta(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
let dLat = transformLat(x: lon - 105.0, y: lat - 35.0)
let dLon = transformLon(x: lon - 105.0, y: lat - 35.0)
let radLat = lat / 180.0 * .pi
var magic = sin(radLat)
magic = 1 - ee * magic * magic
let sqrtMagic = sqrt(magic)
return (
lat: (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * .pi),
lon: (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * .pi)
)
}
private static func transformLat(x: Double, y: Double) -> Double {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
ret += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
ret += (20.0 * sin(y * .pi) + 40.0 * sin(y / 3.0 * .pi)) * 2.0 / 3.0
ret += (160.0 * sin(y / 12.0 * .pi) + 320.0 * sin(y * .pi / 30.0)) * 2.0 / 3.0
return ret
}
private static func transformLon(x: Double, y: Double) -> Double {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
ret += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
ret += (20.0 * sin(x * .pi) + 40.0 * sin(x / 3.0 * .pi)) * 2.0 / 3.0
ret += (150.0 * sin(x / 12.0 * .pi) + 300.0 * sin(x / 30.0 * .pi)) * 2.0 / 3.0
return ret
}
}