mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
feat(location): add China coordinate correction for location simulation
This commit is contained in:
parent
a8a4892a8c
commit
85339f777f
4 changed files with 156 additions and 5 deletions
113
StikDebug/Services/CoordinateConverter.swift
Normal file
113
StikDebug/Services/CoordinateConverter.swift
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// CoordinateConverter.swift
|
||||
// StikDebug
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
/// WGS‑84 ↔ GCJ‑02 (Mars coordinate) bi‑directional conversion.
|
||||
///
|
||||
/// MapKit in mainland China renders on GCJ‑02 tiles provided by Amap
|
||||
/// (高德). Points picked on the map are therefore in GCJ‑02, but
|
||||
/// `simulate_location()` always treats its input as WGS‑84. This utility
|
||||
/// converts GCJ‑02 back to WGS‑84 before simulation so the simulated
|
||||
/// location lands on the correct spot on all map providers.
|
||||
enum CoordinateConverter {
|
||||
|
||||
// MARK: - Constants (from the public domain GCJ‑02 algorithm)
|
||||
|
||||
private static let a = 6378245.0 // semi‑major axis
|
||||
private static let ee = 0.00669342162296594323 // eccentricity squared
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// WGS‑84 → GCJ‑02 (forward).
|
||||
static func wgs84ToGCJ02(_ coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
|
||||
let (lat, lon) = transform(coordinate.latitude, coordinate.longitude, delta: 1)
|
||||
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||
}
|
||||
|
||||
/// GCJ‑02 → WGS‑84 (inverse, via fixed‑point iteration).
|
||||
static func gcj02ToWGS84(_ coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
|
||||
let lat = coordinate.latitude
|
||||
let lon = coordinate.longitude
|
||||
|
||||
// 1st pass
|
||||
let d1 = delta(lat, lon)
|
||||
var wgsLat = lat - d1.lat
|
||||
var wgsLon = lon - d1.lon
|
||||
|
||||
// 2nd pass (refined)
|
||||
let d2 = delta(wgsLat, wgsLon)
|
||||
wgsLat = lat - d2.lat
|
||||
wgsLon = lon - d2.lon
|
||||
|
||||
return CLLocationCoordinate2D(latitude: wgsLat, longitude: wgsLon)
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
private static func transform(_ lat: Double, _ lon: Double, delta: Int) -> (Double, Double) {
|
||||
let d = self.delta(lat, lon)
|
||||
return (lat + Double(delta) * d.lat, lon + Double(delta) * d.lon)
|
||||
}
|
||||
|
||||
private static func delta(_ lat: Double, _ lon: Double) -> (lat: Double, lon: Double) {
|
||||
var dLat = 0.0, dLon = 0.0
|
||||
|
||||
if isOutOfChina(lat, lon) {
|
||||
return (dLat, dLon)
|
||||
}
|
||||
|
||||
dLat = transformLat(lon - 105.0, lat - 35.0)
|
||||
dLon = transformLon(lon - 105.0, lat - 35.0)
|
||||
let radLat = lat / 180.0 * .pi
|
||||
var magic = sin(radLat)
|
||||
magic = 1 - ee * magic * magic
|
||||
let sqrtMagic = sqrt(magic)
|
||||
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * .pi)
|
||||
dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * .pi)
|
||||
return (dLat, dLon)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// Rough bounding‑box check — avoids unnecessary iteration for
|
||||
/// coordinates clearly outside mainland China.
|
||||
private static func isOutOfChina(_ lat: Double, _ lon: Double) -> Bool {
|
||||
// Hong Kong, Macau, and Taiwan are excluded because their
|
||||
// MapKit tiles are *not* GCJ‑02.
|
||||
if lat < 0.8293 || lat > 55.8271 || lon < 72.004 || lon > 137.8347 {
|
||||
return true
|
||||
}
|
||||
// Exclude Hong Kong
|
||||
if lat >= 22.15 && lat <= 22.58 && lon >= 113.82 && lon <= 114.42 {
|
||||
return true
|
||||
}
|
||||
// Exclude Macau
|
||||
if lat >= 22.06 && lat <= 22.22 && lon >= 113.52 && lon <= 113.62 {
|
||||
return true
|
||||
}
|
||||
// Exclude Taiwan
|
||||
if lat >= 21.9 && lat <= 25.4 && lon >= 120.0 && lon <= 122.1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
private enum ExternalLocationAction: Identifiable {
|
||||
case simulate(URL, Double, Double)
|
||||
|
|
@ -54,6 +55,7 @@ struct MainTabView: View {
|
|||
@State private var detachedFeature: AppFeature?
|
||||
@State private var didSetInitialHome = false
|
||||
@State private var pendingLocationAction: ExternalLocationAction?
|
||||
@AppStorage("cnCoordinateCorrection") private var cnCoordinateCorrection = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
|
@ -213,11 +215,24 @@ struct MainTabView: View {
|
|||
return
|
||||
}
|
||||
|
||||
LocationSimulationCommandQueue.shared.async {
|
||||
let simLat: Double
|
||||
let simLon: Double
|
||||
if cnCoordinateCorrection {
|
||||
let corrected = CoordinateConverter.gcj02ToWGS84(
|
||||
CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
)
|
||||
simLat = corrected.latitude
|
||||
simLon = corrected.longitude
|
||||
} else {
|
||||
simLat = coordinate.latitude
|
||||
simLon = coordinate.longitude
|
||||
}
|
||||
|
||||
let workItem = DispatchWorkItem {
|
||||
let code = simulate_location(
|
||||
DeviceConnectionContext.targetIPAddress,
|
||||
coordinate.latitude,
|
||||
coordinate.longitude,
|
||||
simLat,
|
||||
simLon,
|
||||
pairingFile.path
|
||||
)
|
||||
|
||||
|
|
@ -236,10 +251,11 @@ struct MainTabView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
LocationSimulationCommandQueue.shared.async(execute: workItem)
|
||||
}
|
||||
|
||||
private func clearSimulatedLocation() {
|
||||
LocationSimulationCommandQueue.shared.async {
|
||||
let workItem = DispatchWorkItem {
|
||||
let code = clear_simulated_location()
|
||||
DispatchQueue.main.async {
|
||||
if code == 0 {
|
||||
|
|
@ -254,6 +270,7 @@ struct MainTabView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
LocationSimulationCommandQueue.shared.async(execute: workItem)
|
||||
}
|
||||
|
||||
private func coordinate(from url: URL) -> (latitude: Double, longitude: Double)? {
|
||||
|
|
|
|||
|
|
@ -767,6 +767,8 @@ struct LocationSimulationView: View {
|
|||
@State private var showSaveBookmark = false
|
||||
@State private var newBookmarkName = ""
|
||||
|
||||
@AppStorage("cnCoordinateCorrection") private var cnCoordinateCorrection = false
|
||||
|
||||
private var pairingFilePath: String {
|
||||
PairingFileStore.prepareURL().path
|
||||
}
|
||||
|
|
@ -1568,7 +1570,13 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
|
||||
private func locationUpdateCode(for coordinate: CLLocationCoordinate2D) -> Int32 {
|
||||
simulate_location(deviceIP, coordinate.latitude, coordinate.longitude, pairingFilePath)
|
||||
let adjusted: CLLocationCoordinate2D
|
||||
if cnCoordinateCorrection {
|
||||
adjusted = CoordinateConverter.gcj02ToWGS84(coordinate)
|
||||
} else {
|
||||
adjusted = coordinate
|
||||
}
|
||||
return simulate_location(deviceIP, adjusted.latitude, adjusted.longitude, pairingFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct SettingsView: View {
|
|||
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
||||
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
||||
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
|
||||
@AppStorage("cnCoordinateCorrection") private var cnCoordinateCorrection = false
|
||||
@AppStorage(UserDefaults.Keys.targetDeviceIP) private var targetDeviceIP = DeviceConnectionContext.defaultTargetIPAddress
|
||||
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
|
|
@ -131,6 +132,18 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $cnCoordinateCorrection) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("China Coordinate Correction")
|
||||
Text("Fix offset issue of location simulation in China region")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Location")
|
||||
}
|
||||
|
||||
Section("Advanced") {
|
||||
HStack {
|
||||
Text("Target Device IP")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue