fix: 坐标转换修复、图钉键盘对齐、WiFi监控、UI优化

- 坐标转换: detectType 用地理边界判断瓦片类型,加载路径去掉重复 toStored
- 图钉: 放 MKMapView 内部 + keyboard 通知双保险,针尖对齐地理中心
- 缩放控件: 转 UIKit 子视图放地图内,键盘弹起一起移动
- 网络监控: WiFi SSID 轮询 + 重连检测,虚拟定位激活时自动验证
- UI: 去飞行模式弹窗和面板,引导页只在首次安装和设置入口触发
- 实时定位: 蓝点数据优先,不限时缓存
This commit is contained in:
xweiba
2026-08-05 21:20:56 +08:00
parent c8aba2be78
commit 4aa4f63e62
17 changed files with 353 additions and 211 deletions
+98 -11
View File
@@ -2,26 +2,105 @@ import Foundation
/// GCJ-02 () WGS-84
///
/// MKMapView 使 (AutoNavi) GCJ-02
/// MKMapView `centerCoordinate``convert(point:toCoordinateFrom:)`
/// GCJ-02 CoreLocation / CLLocationManager
/// Apple wloc 使 WGS-84
///
///
/// - UI GCJ-02
/// - wloc WGS-84
///
/// UI 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
static func updateTileType(lat: Double, lon: Double) {
let type = detectType(lat: lat, lon: lon)
if type != currentTileType {
currentTileType = type
RuntimeLogger.info("APP", "坐标转换", "地图瓦片切换 → \(type.rawValue)", details: [
"坐标": "\(lat), \(lon)"
])
}
}
// MARK: -
/// WGS-84
@MainActor
static func toStored(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
updateTileType(lat: lat, lon: lon)
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toStored: WGS-84 → 不转", 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", 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) {
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toDisplay: WGS-84 → 不转", 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", 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))
}
/// MKMapView GCJ-02 WGS-84
static func detectType(lat: Double, lon: Double) -> CoordType {
// GCJ-02
if lat > 17.5 && lat < 54.0 && lon > 72.5 && lon < 136.0 {
return .gcj02
}
return .wgs84
}
// 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
@@ -30,6 +109,14 @@ enum CoordinateConverter {
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)
+41 -2
View File
@@ -1,5 +1,6 @@
import Network
import Foundation
import SystemConfiguration.CaptiveNetwork
@MainActor
final class NetworkMonitor: ObservableObject {
@@ -7,20 +8,58 @@ final class NetworkMonitor: ObservableObject {
@Published private(set) var isSatisfied = true
@Published private(set) var isWiFiEnabled = true
@Published private(set) var currentSSID: String?
/// WiFi SSID 使
var onWiFiChanged: (() -> Void)?
private let monitor = NWPathMonitor()
private var ssidTimer: Timer?
private var wasSatisfied = true
private init() {
monitor.pathUpdateHandler = { [weak self] path in
let satisfied = path.status == .satisfied
let wifi = path.usesInterfaceType(.wifi)
Task { @MainActor in
self?.isSatisfied = satisfied
self?.isWiFiEnabled = wifi
guard let self else { return }
//
let reconnected = satisfied && !self.wasSatisfied && wifi
self.wasSatisfied = satisfied
self.isSatisfied = satisfied
self.isWiFiEnabled = wifi
if reconnected {
self.onWiFiChanged?()
}
}
}
monitor.start(queue: .main)
startSSIDPolling()
}
var isAirplaneMode: Bool { !isSatisfied }
private func startSSIDPolling() {
ssidTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
Task { @MainActor in
guard let self else { return }
let ssid = Self.fetchSSID()
if ssid != self.currentSSID, ssid != nil {
self.currentSSID = ssid
self.onWiFiChanged?()
}
}
}
}
static func fetchSSID() -> String? {
guard let interfaces = CNCopySupportedInterfaces() as? [String] else { return nil }
for iface in interfaces {
if let info = CNCopyCurrentNetworkInfo(iface as CFString) as? [String: Any],
let ssid = info[kCNNetworkInfoKeySSID as String] as? String {
return ssid
}
}
return nil
}
}