mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Convert GCJ-02 map coordinates to WGS-84 before simulating location
In mainland China, published map data is legally required to use the GCJ-02 datum, so MapKit renders AutoNavi tiles and hands back GCJ-02 coordinates. The location simulation service expects WGS-84, so a point picked on the map is currently simulated 100-700 m away from where the user tapped. Convert at locationUpdateCode(for:), the single place where map-derived coordinates -- both the dropped pin and route playback -- leave MapSelectionView. The URL-scheme path in MainTabView is deliberately left alone: stikdebug://simulate-location carries raw WGS-84 from outside the app, so converting there would double-shift it. Coordinates outside the mainland bounding box are returned unchanged, so behaviour everywhere else is bit-for-bit identical. Measured at Tiananmen Square (39.9087, 116.3975): 556.1 m of offset removed; round-trip error 0.000016 m.
This commit is contained in:
parent
94bc9e8cf3
commit
d9c4c07eb5
2 changed files with 113 additions and 1 deletions
107
StikDebug/Core/ChinaCoordinateTransform.swift
Normal file
107
StikDebug/Core/ChinaCoordinateTransform.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// ChinaCoordinateTransform.swift
|
||||
// StikDebug
|
||||
//
|
||||
// Inside mainland China, map data is legally required to use the GCJ-02
|
||||
// ("Mars") datum, so MapKit renders AutoNavi tiles and hands back GCJ-02
|
||||
// coordinates. The location simulation service expects WGS-84. Feeding a
|
||||
// GCJ-02 coordinate straight through therefore lands 100-700 m away from
|
||||
// the point the user tapped.
|
||||
//
|
||||
// This converts at the single boundary where map coordinates leave the
|
||||
// picker. Coordinates outside the mainland bounding box pass through
|
||||
// untouched, so nothing changes for users elsewhere.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
enum ChinaCoordinateTransform {
|
||||
|
||||
/// Krasovsky 1940 semi-major axis used by the GCJ-02 obfuscation.
|
||||
private static let semiMajorAxis = 6_378_245.0
|
||||
/// Square of the first eccentricity for the same ellipsoid.
|
||||
private static let eccentricitySquared = 0.006_693_421_622_965_943
|
||||
|
||||
/// Rough bounding box of mainland China. Deliberately generous: the
|
||||
/// GCJ-02 offset is at most ~700 m, so a coordinate that is genuinely
|
||||
/// outside cannot be pushed inside by the transform.
|
||||
static func isOutsideMainlandChina(latitude: Double, longitude: Double) -> Bool {
|
||||
!(longitude > 73.66 && longitude < 135.05 && latitude > 3.86 && latitude < 53.55)
|
||||
}
|
||||
|
||||
/// WGS-84 -> GCJ-02. Closed form.
|
||||
static func gcj02(fromWGS84 coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
|
||||
guard !isOutsideMainlandChina(latitude: coordinate.latitude, longitude: coordinate.longitude) else {
|
||||
return coordinate
|
||||
}
|
||||
|
||||
let x = coordinate.longitude - 105.0
|
||||
let y = coordinate.latitude - 35.0
|
||||
|
||||
var deltaLatitude = latitudeOffset(x: x, y: y)
|
||||
var deltaLongitude = longitudeOffset(x: x, y: y)
|
||||
|
||||
let radianLatitude = coordinate.latitude / 180.0 * .pi
|
||||
var magic = sin(radianLatitude)
|
||||
magic = 1 - eccentricitySquared * magic * magic
|
||||
let sqrtMagic = sqrt(magic)
|
||||
|
||||
deltaLatitude = (deltaLatitude * 180.0)
|
||||
/ ((semiMajorAxis * (1 - eccentricitySquared)) / (magic * sqrtMagic) * .pi)
|
||||
deltaLongitude = (deltaLongitude * 180.0)
|
||||
/ (semiMajorAxis / sqrtMagic * cos(radianLatitude) * .pi)
|
||||
|
||||
return CLLocationCoordinate2D(
|
||||
latitude: coordinate.latitude + deltaLatitude,
|
||||
longitude: coordinate.longitude + deltaLongitude
|
||||
)
|
||||
}
|
||||
|
||||
/// GCJ-02 -> WGS-84. The forward transform has no analytic inverse, so
|
||||
/// this iterates on the residual; it converges well below a metre in a
|
||||
/// handful of passes.
|
||||
static func wgs84(fromGCJ02 coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
|
||||
guard !isOutsideMainlandChina(latitude: coordinate.latitude, longitude: coordinate.longitude) else {
|
||||
return coordinate
|
||||
}
|
||||
|
||||
var estimate = coordinate
|
||||
// ~1e-9 degrees is well under a millimetre; the loop normally exits
|
||||
// after three or four passes.
|
||||
let tolerance = 1e-9
|
||||
|
||||
for _ in 0..<10 {
|
||||
let roundTrip = gcj02(fromWGS84: estimate)
|
||||
let latitudeError = roundTrip.latitude - coordinate.latitude
|
||||
let longitudeError = roundTrip.longitude - coordinate.longitude
|
||||
|
||||
if abs(latitudeError) < tolerance && abs(longitudeError) < tolerance {
|
||||
break
|
||||
}
|
||||
|
||||
estimate = CLLocationCoordinate2D(
|
||||
latitude: estimate.latitude - latitudeError,
|
||||
longitude: estimate.longitude - longitudeError
|
||||
)
|
||||
}
|
||||
|
||||
return estimate
|
||||
}
|
||||
|
||||
private static func latitudeOffset(x: Double, y: Double) -> Double {
|
||||
var offset = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
|
||||
offset += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
|
||||
offset += (20.0 * sin(y * .pi) + 40.0 * sin(y / 3.0 * .pi)) * 2.0 / 3.0
|
||||
offset += (160.0 * sin(y / 12.0 * .pi) + 320.0 * sin(y * .pi / 30.0)) * 2.0 / 3.0
|
||||
return offset
|
||||
}
|
||||
|
||||
private static func longitudeOffset(x: Double, y: Double) -> Double {
|
||||
var offset = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
|
||||
offset += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
|
||||
offset += (20.0 * sin(x * .pi) + 40.0 * sin(x / 3.0 * .pi)) * 2.0 / 3.0
|
||||
offset += (150.0 * sin(x / 12.0 * .pi) + 300.0 * sin(x / 30.0 * .pi)) * 2.0 / 3.0
|
||||
return offset
|
||||
}
|
||||
}
|
||||
|
|
@ -1568,7 +1568,12 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
|
||||
private func locationUpdateCode(for coordinate: CLLocationCoordinate2D) -> Int32 {
|
||||
simulate_location(deviceIP, coordinate.latitude, coordinate.longitude, pairingFilePath)
|
||||
// Map coordinates arrive in whatever datum MapKit is rendering. Inside
|
||||
// mainland China that is GCJ-02, while the simulation service expects
|
||||
// WGS-84, so convert here -- the single point where picked coordinates
|
||||
// leave this view. Elsewhere this is a no-op.
|
||||
let wgs84 = ChinaCoordinateTransform.wgs84(fromGCJ02: coordinate)
|
||||
return simulate_location(deviceIP, wgs84.latitude, wgs84.longitude, pairingFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue