mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
merge: integrate complete MapKit runtime refresh
# Conflicts: # App/MapHomeView.swift # Shared/CoordinateConverter.swift # Tests/PaopaoLocationSpooferTests/FavoriteLocationStoreTests.swift # Tests/map_refactor_contract_test.sh
This commit is contained in:
+223
-116
@@ -79,19 +79,20 @@ struct MapHomeView: View {
|
|||||||
@State private var wifiChangeObserverToken: UUID?
|
@State private var wifiChangeObserverToken: UUID?
|
||||||
@State private var wifiVerificationTask: Task<Void, Never>?
|
@State private var wifiVerificationTask: Task<Void, Never>?
|
||||||
@State private var wifiVerificationID: UUID?
|
@State private var wifiVerificationID: UUID?
|
||||||
@State private var copyConfirmed = false
|
@State private var copiedCoordinateSystem: CoordinateConverter.MapCoordinateSystem?
|
||||||
@State private var spoofState: SpoofState = .idle
|
@State private var spoofState: SpoofState = .idle
|
||||||
@State private var locationOperationTask: Task<Void, Never>?
|
@State private var locationOperationTask: Task<Void, Never>?
|
||||||
@State private var locationOperationID: UInt64 = 0
|
@State private var locationOperationID: UInt64 = 0
|
||||||
|
@State private var mapCoordinateSystemRefreshTask: Task<Void, Never>?
|
||||||
|
@State private var mapCoordinateSystemRefreshID: UInt64 = 0
|
||||||
|
@State private var bluePointRefreshPending = false
|
||||||
|
@State private var realtimeButtonTask: Task<Void, Never>?
|
||||||
|
@State private var favoriteSaveTask: Task<Void, Never>?
|
||||||
// 激活时的坐标(本地存,绕过 C 桥接层精度丢失)
|
// 激活时的坐标(本地存,绕过 C 桥接层精度丢失)
|
||||||
@State private var activeSpoofLat: Double?
|
@State private var activeSpoofLat: Double?
|
||||||
@State private var activeSpoofLon: Double?
|
@State private var activeSpoofLon: Double?
|
||||||
@State private var lastSpoofDiagnosisSystem: CoordinateConverter.MapCoordinateSystem?
|
@State private var lastSpoofDiagnosisSystem: CoordinateConverter.MapCoordinateSystem?
|
||||||
@State private var hasLoggedSpoofDiagnosis = false
|
@State private var hasLoggedSpoofDiagnosis = false
|
||||||
@State private var coordinateSystemRefreshTask: Task<Void, Never>?
|
|
||||||
@State private var coordinateSystemRefreshTaskID: UUID?
|
|
||||||
@State private var coordinateSystemRefreshAttempts = 0
|
|
||||||
@State private var lastCoordinateSystemRefreshAt: Date?
|
|
||||||
|
|
||||||
init(setup: SetupCoordinator) {
|
init(setup: SetupCoordinator) {
|
||||||
self.setup = setup
|
self.setup = setup
|
||||||
@@ -237,7 +238,7 @@ struct MapHomeView: View {
|
|||||||
.shadow(color: .black.opacity(0.2), radius: 6, y: 3)
|
.shadow(color: .black.opacity(0.2), radius: 6, y: 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(realtimeRequestTask != nil || realtime.isRequesting)
|
.disabled(realtimeButtonTask != nil || realtimeRequestTask != nil || realtime.isRequesting)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
@@ -284,19 +285,24 @@ struct MapHomeView: View {
|
|||||||
wifiVerificationTask?.cancel()
|
wifiVerificationTask?.cancel()
|
||||||
wifiVerificationTask = nil
|
wifiVerificationTask = nil
|
||||||
wifiVerificationID = nil
|
wifiVerificationID = nil
|
||||||
resetMapCoordinateSystemRefresh()
|
mapCoordinateSystemRefreshTask?.cancel()
|
||||||
|
mapCoordinateSystemRefreshTask = nil
|
||||||
|
bluePointRefreshPending = false
|
||||||
|
realtimeButtonTask?.cancel()
|
||||||
|
realtimeButtonTask = nil
|
||||||
|
favoriteSaveTask?.cancel()
|
||||||
|
favoriteSaveTask = nil
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
guard phase == .active else { return }
|
guard phase == .active else { return }
|
||||||
resetMapCoordinateSystemRefresh()
|
Task { @MainActor in
|
||||||
refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "App回到前台")
|
await awaitCoordinatedMapCoordinateSystemRefresh(reason: "App回到前台")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: proxy.isRunning) { running in
|
.onChange(of: proxy.isRunning) { running in
|
||||||
if runtimeMode.mode == .localWiFi, !running && spoofState == .active {
|
if runtimeMode.mode == .localWiFi, !running && spoofState == .active {
|
||||||
spoofState = .idle
|
spoofState = .idle
|
||||||
actions.clear()
|
actions.clear()
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "本机代理意外停止")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: runtimeMode.mode) { mode in
|
.onChange(of: runtimeMode.mode) { mode in
|
||||||
@@ -309,7 +315,6 @@ struct MapHomeView: View {
|
|||||||
}
|
}
|
||||||
wifiVerificationTask?.cancel()
|
wifiVerificationTask?.cancel()
|
||||||
wifiVerificationTask = nil
|
wifiVerificationTask = nil
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
activeSpoofLat = nil
|
activeSpoofLat = nil
|
||||||
activeSpoofLon = nil
|
activeSpoofLon = nil
|
||||||
if mode == .localWiFi {
|
if mode == .localWiFi {
|
||||||
@@ -426,27 +431,8 @@ struct MapHomeView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(mapState.displayName ?? "当前选点").font(.subheadline.weight(.semibold)).lineLimit(1)
|
Text(mapState.displayName ?? "当前选点").font(.subheadline.weight(.semibold)).lineLimit(1)
|
||||||
Text(String(format: "%.6f, %.6f", mapState.selection.coordinate.latitude, mapState.selection.coordinate.longitude))
|
coordinateRow(label: "国内标准 GCJ-02", system: .gcj02)
|
||||||
.font(.caption.monospaced())
|
coordinateRow(label: "国际标准 WGS-84", system: .wgs84)
|
||||||
.foregroundStyle(copyConfirmed ? .green : .secondary)
|
|
||||||
.onTapGesture {
|
|
||||||
let text = String(format: "%.6f, %.6f", mapState.selection.coordinate.latitude, mapState.selection.coordinate.longitude)
|
|
||||||
UIPasteboard.general.string = text
|
|
||||||
RuntimeLogger.info("APP", "地图", "已复制坐标")
|
|
||||||
copyConfirmed = true
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copyConfirmed = false }
|
|
||||||
}
|
|
||||||
.overlay(alignment: .top) {
|
|
||||||
if copyConfirmed {
|
|
||||||
Text("已复制")
|
|
||||||
.font(.caption2.bold())
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(.green, in: Capsule())
|
|
||||||
.offset(y: -24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
// 帮助说明按钮
|
// 帮助说明按钮
|
||||||
@@ -471,23 +457,7 @@ struct MapHomeView: View {
|
|||||||
favorites.select(nil)
|
favorites.select(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let snapshot = currentSelectionFavorite
|
saveCurrentSelectionAsFavorite()
|
||||||
RuntimeLogger.info("APP", "坐标转换", "保存当前选点为收藏", details: [
|
|
||||||
"当前地图标准": CoordinateConverter.currentMapCoordinateSystem.diagnosticName,
|
|
||||||
"输入字段": CoordinateConverter.currentMapCoordinateSystem.diagnosticName,
|
|
||||||
"持久化字段": "国际标准(WGS-84)+国内标准(GCJ-02)"
|
|
||||||
])
|
|
||||||
let favorite = favorites.save(
|
|
||||||
name: snapshot.name,
|
|
||||||
mapCoordinate: mapState.selection.coordinate,
|
|
||||||
mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem,
|
|
||||||
accuracy: snapshot.accuracy
|
|
||||||
)
|
|
||||||
mapState.selectFavorite(
|
|
||||||
favorite.coordinatePair.coordinate(for: CoordinateConverter.currentMapCoordinateSystem),
|
|
||||||
id: favorite.id,
|
|
||||||
name: favorite.name
|
|
||||||
)
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: favorites.selectedFavoriteID != nil ? "star.fill" : "star")
|
Image(systemName: favorites.selectedFavoriteID != nil ? "star.fill" : "star")
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.system(size: 18, weight: .semibold))
|
||||||
@@ -496,6 +466,7 @@ struct MapHomeView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray)
|
.foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray)
|
||||||
|
.disabled(favoriteSaveTask != nil)
|
||||||
.accessibilityLabel(favorites.selectedFavoriteID != nil ? "已收藏,点击取消收藏" : "收藏当前选点")
|
.accessibilityLabel(favorites.selectedFavoriteID != nil ? "已收藏,点击取消收藏" : "收藏当前选点")
|
||||||
}
|
}
|
||||||
// 收藏
|
// 收藏
|
||||||
@@ -616,7 +587,6 @@ struct MapHomeView: View {
|
|||||||
spoofState = .active
|
spoofState = .active
|
||||||
activeSpoofLat = response.latitude
|
activeSpoofLat = response.latitude
|
||||||
activeSpoofLon = response.longitude
|
activeSpoofLon = response.longitude
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
RuntimeLogger.info("APP", "定位", "第三方代理坐标同步成功", details: [
|
RuntimeLogger.info("APP", "定位", "第三方代理坐标同步成功", details: [
|
||||||
"坐标标准": "WGS-84",
|
"坐标标准": "WGS-84",
|
||||||
"客户端模式": "测试模式",
|
"客户端模式": "测试模式",
|
||||||
@@ -655,7 +625,6 @@ struct MapHomeView: View {
|
|||||||
activeSpoofLon = target.longitude
|
activeSpoofLon = target.longitude
|
||||||
lastSpoofDiagnosisSystem = nil
|
lastSpoofDiagnosisSystem = nil
|
||||||
hasLoggedSpoofDiagnosis = false
|
hasLoggedSpoofDiagnosis = false
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
}
|
}
|
||||||
RuntimeLogger.info("APP", "定位", "验证结果", details: [
|
RuntimeLogger.info("APP", "定位", "验证结果", details: [
|
||||||
"success": "true",
|
"success": "true",
|
||||||
@@ -695,8 +664,6 @@ struct MapHomeView: View {
|
|||||||
spoofState = .idle
|
spoofState = .idle
|
||||||
activeSpoofLat = nil
|
activeSpoofLat = nil
|
||||||
activeSpoofLon = nil
|
activeSpoofLon = nil
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "第三方虚拟定位已停用")
|
|
||||||
presentSuccessfulOperationTip(.deactivation)
|
presentSuccessfulOperationTip(.deactivation)
|
||||||
} catch {
|
} catch {
|
||||||
spoofState = .active
|
spoofState = .active
|
||||||
@@ -713,8 +680,6 @@ struct MapHomeView: View {
|
|||||||
activeSpoofLon = nil
|
activeSpoofLon = nil
|
||||||
lastSpoofDiagnosisSystem = nil
|
lastSpoofDiagnosisSystem = nil
|
||||||
hasLoggedSpoofDiagnosis = false
|
hasLoggedSpoofDiagnosis = false
|
||||||
resetMapCoordinateSystemRefresh()
|
|
||||||
refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "APP虚拟定位已停用")
|
|
||||||
presentSuccessfulOperationTip(.deactivation)
|
presentSuccessfulOperationTip(.deactivation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +746,58 @@ struct MapHomeView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var currentSelectionPair: CoordinatePair {
|
||||||
|
if let stored = LastCoordinateStore.load(),
|
||||||
|
stored.coordinate(for: CoordinateConverter.currentMapCoordinateSystem)
|
||||||
|
.isApproximatelyEqual(to: mapState.selection.coordinate) {
|
||||||
|
return stored.coordinatePair
|
||||||
|
}
|
||||||
|
return CoordinatePair(
|
||||||
|
mapCoordinate: mapState.selection.coordinate,
|
||||||
|
mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func coordinateRow(
|
||||||
|
label: String,
|
||||||
|
system: CoordinateConverter.MapCoordinateSystem
|
||||||
|
) -> some View {
|
||||||
|
let coordinate = currentSelectionPair.coordinate(for: system)
|
||||||
|
let text = String(format: "%.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
||||||
|
return HStack(spacing: 6) {
|
||||||
|
Text(label)
|
||||||
|
.font(.caption2.weight(.medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: 112, alignment: .leading)
|
||||||
|
Text(text)
|
||||||
|
.font(.caption.monospaced())
|
||||||
|
.foregroundStyle(copiedCoordinateSystem == system ? .green : .secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
UIPasteboard.general.string = text
|
||||||
|
copiedCoordinateSystem = system
|
||||||
|
RuntimeLogger.info("APP", "地图", "已复制坐标", details: [
|
||||||
|
"坐标标准": system.diagnosticName
|
||||||
|
])
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||||
|
if copiedCoordinateSystem == system { copiedCoordinateSystem = nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay(alignment: .topTrailing) {
|
||||||
|
if copiedCoordinateSystem == system {
|
||||||
|
Text("已复制")
|
||||||
|
.font(.caption2.bold())
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(.green, in: Capsule())
|
||||||
|
.offset(y: -24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var testFavorite: FavoriteLocation { currentSelectionFavorite }
|
private var testFavorite: FavoriteLocation { currentSelectionFavorite }
|
||||||
|
|
||||||
private func startMapRuntimeOnce() {
|
private func startMapRuntimeOnce() {
|
||||||
@@ -794,19 +811,152 @@ struct MapHomeView: View {
|
|||||||
scheduleGeocode(pair: pair, revision: mapState.selection.revision)
|
scheduleGeocode(pair: pair, revision: mapState.selection.revision)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reprojectMapSelection(for change: CoordinateConverter.MapCoordinateSystemChange) {
|
@discardableResult
|
||||||
|
private func reprojectMapSelection(for change: CoordinateConverter.MapCoordinateSystemChange) -> Bool {
|
||||||
// Every current selection is persisted as a complete coordinate pair at
|
// Every current selection is persisted as a complete coordinate pair at
|
||||||
// its input boundary. Replaying the matching stored representation
|
// its input boundary. Replaying the matching stored representation
|
||||||
// avoids a second GCJ/WGS conversion and its accumulated offset.
|
// avoids a second GCJ/WGS conversion and its accumulated offset.
|
||||||
guard let stored = LastCoordinateStore.load() else {
|
guard let stored = LastCoordinateStore.load() else {
|
||||||
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准切换时未找到当前选点缓存")
|
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准切换时未找到当前选点缓存")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
mapState.reprojectSelectionForMapCoordinateSystemChange(stored.coordinate(for: change.current))
|
mapState.reprojectSelectionForMapCoordinateSystemChange(stored.coordinate(for: change.current))
|
||||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准切换后已使用缓存坐标对回显当前选点", details: [
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准切换后已使用缓存坐标对回显当前选点", details: [
|
||||||
"from": change.previous.rawValue,
|
"from": change.previous.rawValue,
|
||||||
"to": change.current.rawValue
|
"to": change.current.rawValue
|
||||||
])
|
])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyRuntimeMapCoordinateSystemChange(
|
||||||
|
_ change: CoordinateConverter.MapCoordinateSystemChange,
|
||||||
|
reason: String
|
||||||
|
) {
|
||||||
|
geocodeDebounceTask?.cancel()
|
||||||
|
reverseGeocodeTask?.cancel()
|
||||||
|
searchRequestID &+= 1
|
||||||
|
isSearching = false
|
||||||
|
searchResults = []
|
||||||
|
searchError = ""
|
||||||
|
realtimeRequestTask?.cancel()
|
||||||
|
realtimeRequestTask = nil
|
||||||
|
realtimeRequestContext = nil
|
||||||
|
mapState.clearRealtimeLocationForMapCoordinateSystemChange()
|
||||||
|
let pinWasReprojected = reprojectMapSelection(for: change)
|
||||||
|
if let stored = LastCoordinateStore.load() {
|
||||||
|
scheduleGeocode(pair: stored.coordinatePair, revision: mapState.selection.revision)
|
||||||
|
}
|
||||||
|
RuntimeLogger.warning("APP", "坐标转换", "地图坐标类型已变化", details: [
|
||||||
|
"触发原因": reason,
|
||||||
|
"旧类型": change.previous.diagnosticName,
|
||||||
|
"新类型": change.current.diagnosticName,
|
||||||
|
"图钉已按新类型重设": String(pinWasReprojected),
|
||||||
|
"蓝点缓存": "已清理",
|
||||||
|
"搜索结果": "已清理",
|
||||||
|
"异步地理编码": "已重置"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func refreshRuntimeMapCoordinateSystem(reason: String) async -> Bool {
|
||||||
|
let result = await CoordinateConverter.refreshRuntimeMapCoordinateSystem(reason: reason)
|
||||||
|
guard !Task.isCancelled else { return false }
|
||||||
|
switch result {
|
||||||
|
case .changed(let change):
|
||||||
|
applyRuntimeMapCoordinateSystemChange(change, reason: reason)
|
||||||
|
return true
|
||||||
|
case .unchanged:
|
||||||
|
return true
|
||||||
|
case .unavailable, .cancelled:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleBluePointMapCoordinateSystemRefresh() {
|
||||||
|
guard mapCoordinateSystemRefreshTask == nil else {
|
||||||
|
bluePointRefreshPending = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapCoordinateSystemRefreshID &+= 1
|
||||||
|
let refreshID = mapCoordinateSystemRefreshID
|
||||||
|
mapCoordinateSystemRefreshTask = Task { @MainActor in
|
||||||
|
defer {
|
||||||
|
if refreshID == mapCoordinateSystemRefreshID {
|
||||||
|
let needsAnotherRefresh = bluePointRefreshPending && !Task.isCancelled
|
||||||
|
mapCoordinateSystemRefreshTask = nil
|
||||||
|
bluePointRefreshPending = false
|
||||||
|
if needsAnotherRefresh {
|
||||||
|
scheduleBluePointMapCoordinateSystemRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Coalesce the didUpdate/regionDidChange pair generated by one
|
||||||
|
// native location sample without caching the probe result.
|
||||||
|
try? await Task.sleep(nanoseconds: 350_000_000)
|
||||||
|
guard !Task.isCancelled, refreshID == mapCoordinateSystemRefreshID else { return }
|
||||||
|
var attempt = 0
|
||||||
|
repeat {
|
||||||
|
bluePointRefreshPending = false
|
||||||
|
_ = await refreshRuntimeMapCoordinateSystem(reason: "MapKit蓝点新样本")
|
||||||
|
attempt += 1
|
||||||
|
} while !Task.isCancelled
|
||||||
|
&& refreshID == mapCoordinateSystemRefreshID
|
||||||
|
&& bluePointRefreshPending
|
||||||
|
&& attempt < 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func awaitCoordinatedMapCoordinateSystemRefresh(reason: String) async {
|
||||||
|
if let pendingRefresh = mapCoordinateSystemRefreshTask {
|
||||||
|
await pendingRefresh.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapCoordinateSystemRefreshID &+= 1
|
||||||
|
let refreshID = mapCoordinateSystemRefreshID
|
||||||
|
let task = Task { @MainActor in
|
||||||
|
_ = await refreshRuntimeMapCoordinateSystem(reason: reason)
|
||||||
|
}
|
||||||
|
mapCoordinateSystemRefreshTask = task
|
||||||
|
await task.value
|
||||||
|
if refreshID == mapCoordinateSystemRefreshID {
|
||||||
|
mapCoordinateSystemRefreshTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveCurrentSelectionAsFavorite() {
|
||||||
|
guard favoriteSaveTask == nil else { return }
|
||||||
|
let snapshot = currentSelectionFavorite
|
||||||
|
// Preserve the pair created when this selection entered the map. If
|
||||||
|
// the runtime probe changes type, replay its other stored field instead
|
||||||
|
// of reinterpreting the old visible coordinate as the new type.
|
||||||
|
let pair = currentSelectionPair
|
||||||
|
let selectionRevision = mapState.selection.revision
|
||||||
|
favoriteSaveTask = Task { @MainActor in
|
||||||
|
defer { favoriteSaveTask = nil }
|
||||||
|
await awaitCoordinatedMapCoordinateSystemRefresh(reason: "保存收藏")
|
||||||
|
guard !Task.isCancelled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard mapState.selection.revision == selectionRevision else {
|
||||||
|
RuntimeLogger.info("APP", "坐标转换", "取消保存收藏:检测期间当前选点已变化")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
RuntimeLogger.info("APP", "坐标转换", "保存当前选点为收藏", details: [
|
||||||
|
"当前地图标准": CoordinateConverter.currentMapCoordinateSystem.diagnosticName,
|
||||||
|
"持久化字段": "国际标准(WGS-84)+国内标准(GCJ-02)"
|
||||||
|
])
|
||||||
|
let favorite = favorites.save(
|
||||||
|
name: snapshot.name,
|
||||||
|
coordinatePair: pair,
|
||||||
|
accuracy: snapshot.accuracy
|
||||||
|
)
|
||||||
|
mapState.selectFavorite(
|
||||||
|
pair.coordinate(for: CoordinateConverter.currentMapCoordinateSystem),
|
||||||
|
id: favorite.id,
|
||||||
|
name: favorite.name
|
||||||
|
)
|
||||||
|
LastCoordinateStore.save(coordinatePair: pair, zoomMeters: mapState.viewportMeters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func registerWiFiChangeObserver() {
|
private func registerWiFiChangeObserver() {
|
||||||
@@ -936,6 +1086,18 @@ struct MapHomeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func requestRealtimeLocation() {
|
private func requestRealtimeLocation() {
|
||||||
|
guard realtimeButtonTask == nil else { return }
|
||||||
|
realtimeButtonTask = Task { @MainActor in
|
||||||
|
defer { realtimeButtonTask = nil }
|
||||||
|
await awaitCoordinatedMapCoordinateSystemRefresh(reason: "点击实时定位")
|
||||||
|
guard !Task.isCancelled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
performRealtimeLocationRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performRealtimeLocationRequest() {
|
||||||
let intent = mapState.beginRealtimeIntent()
|
let intent = mapState.beginRealtimeIntent()
|
||||||
RuntimeLogger.info("APP", "实时定位", "用户点击实时定位", details: [
|
RuntimeLogger.info("APP", "实时定位", "用户点击实时定位", details: [
|
||||||
"intentID": String(intent.id),
|
"intentID": String(intent.id),
|
||||||
@@ -971,6 +1133,7 @@ struct MapHomeView: View {
|
|||||||
private func handleNativeRealtimeLocation(_ location: CLLocation) {
|
private func handleNativeRealtimeLocation(_ location: CLLocation) {
|
||||||
mapState.updateRealtimeLocation(location)
|
mapState.updateRealtimeLocation(location)
|
||||||
logSpoofCoordinateDiagnosisIfNeeded(location)
|
logSpoofCoordinateDiagnosisIfNeeded(location)
|
||||||
|
scheduleBluePointMapCoordinateSystemRefresh()
|
||||||
guard let context = realtimeRequestContext else {
|
guard let context = realtimeRequestContext else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1005,17 +1168,12 @@ struct MapHomeView: View {
|
|||||||
lon: longitude,
|
lon: longitude,
|
||||||
mapCoordinateSystem: .wgs84
|
mapCoordinateSystem: .wgs84
|
||||||
)
|
)
|
||||||
let maximumTargetDistance = max(1_000, location.horizontalAccuracy * 4)
|
|
||||||
let diagnosis = CoordinateConverter.diagnoseRepresentation(
|
let diagnosis = CoordinateConverter.diagnoseRepresentation(
|
||||||
sample: location.coordinate,
|
sample: location.coordinate,
|
||||||
pair: targetPair,
|
pair: targetPair,
|
||||||
maximumDistance: maximumTargetDistance,
|
maximumDistance: max(1_000, location.horizontalAccuracy * 4),
|
||||||
minimumSeparation: max(30, location.horizontalAccuracy)
|
minimumSeparation: max(30, location.horizontalAccuracy)
|
||||||
)
|
)
|
||||||
let nearestTargetDistance = min(diagnosis.distanceToWGS84, diagnosis.distanceToGCJ02)
|
|
||||||
if nearestTargetDistance <= maximumTargetDistance {
|
|
||||||
refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: "虚拟定位蓝点已到达目标")
|
|
||||||
}
|
|
||||||
let shouldLog = !hasLoggedSpoofDiagnosis
|
let shouldLog = !hasLoggedSpoofDiagnosis
|
||||||
|| diagnosis.inferredSystem != lastSpoofDiagnosisSystem
|
|| diagnosis.inferredSystem != lastSpoofDiagnosisSystem
|
||||||
guard shouldLog else { return }
|
guard shouldLog else { return }
|
||||||
@@ -1027,52 +1185,10 @@ struct MapHomeView: View {
|
|||||||
"蓝点回调更接近": diagnosis.inferredName,
|
"蓝点回调更接近": diagnosis.inferredName,
|
||||||
"蓝点距WGS目标米": String(format: "%.1f", diagnosis.distanceToWGS84),
|
"蓝点距WGS目标米": String(format: "%.1f", diagnosis.distanceToWGS84),
|
||||||
"蓝点距GCJ目标米": String(format: "%.1f", diagnosis.distanceToGCJ02),
|
"蓝点距GCJ目标米": String(format: "%.1f", diagnosis.distanceToGCJ02),
|
||||||
"蓝点已到达目标": String(nearestTargetDistance <= maximumTargetDistance),
|
|
||||||
"日志策略": "每次开启首次或判定变化"
|
"日志策略": "每次开启首次或判定变化"
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetMapCoordinateSystemRefresh() {
|
|
||||||
coordinateSystemRefreshTask?.cancel()
|
|
||||||
coordinateSystemRefreshTask = nil
|
|
||||||
coordinateSystemRefreshTaskID = nil
|
|
||||||
coordinateSystemRefreshAttempts = 0
|
|
||||||
lastCoordinateSystemRefreshAt = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshMapCoordinateSystemAfterLocationEnvironmentChange(trigger: String) {
|
|
||||||
guard coordinateSystemRefreshTask == nil,
|
|
||||||
coordinateSystemRefreshAttempts < 3 else { return }
|
|
||||||
if let lastCoordinateSystemRefreshAt,
|
|
||||||
Date().timeIntervalSince(lastCoordinateSystemRefreshAt) < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
coordinateSystemRefreshAttempts += 1
|
|
||||||
lastCoordinateSystemRefreshAt = Date()
|
|
||||||
let attempt = coordinateSystemRefreshAttempts
|
|
||||||
let taskID = UUID()
|
|
||||||
coordinateSystemRefreshTaskID = taskID
|
|
||||||
coordinateSystemRefreshTask = Task { @MainActor in
|
|
||||||
defer {
|
|
||||||
if coordinateSystemRefreshTaskID == taskID {
|
|
||||||
coordinateSystemRefreshTask = nil
|
|
||||||
coordinateSystemRefreshTaskID = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let change = await CoordinateConverter.refreshMapCoordinateSystem()
|
|
||||||
guard !Task.isCancelled, coordinateSystemRefreshTaskID == taskID else { return }
|
|
||||||
if let change {
|
|
||||||
reprojectMapSelection(for: change)
|
|
||||||
}
|
|
||||||
RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新请求结束", details: [
|
|
||||||
"触发": trigger,
|
|
||||||
"尝试": String(attempt),
|
|
||||||
"发生切换": String(change != nil),
|
|
||||||
"当前标准": CoordinateConverter.currentMapCoordinateSystem.rawValue
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startRealtimeLocationRequest(
|
private func startRealtimeLocationRequest(
|
||||||
source: String,
|
source: String,
|
||||||
showFailureAlert: Bool,
|
showFailureAlert: Bool,
|
||||||
@@ -1146,14 +1262,7 @@ struct MapHomeView: View {
|
|||||||
sourceDescription: String
|
sourceDescription: String
|
||||||
) {
|
) {
|
||||||
let currentViewport = mapState.viewportMeters
|
let currentViewport = mapState.viewportMeters
|
||||||
let previousMapCoordinateSystem = CoordinateConverter.currentMapCoordinateSystem
|
|
||||||
let sourceCoordinateSystem = source.coordinateSystem
|
let sourceCoordinateSystem = source.coordinateSystem
|
||||||
let mapCoordinateSystemChange = source == .coreLocation
|
|
||||||
? CoordinateConverter.correctMapCoordinateSystemUsingRealtime(coordinate)
|
|
||||||
: nil
|
|
||||||
if let change = mapCoordinateSystemChange {
|
|
||||||
reprojectMapSelection(for: change)
|
|
||||||
}
|
|
||||||
let pair = CoordinateConverter.coordinatePair(
|
let pair = CoordinateConverter.coordinatePair(
|
||||||
lat: coordinate.latitude,
|
lat: coordinate.latitude,
|
||||||
lon: coordinate.longitude,
|
lon: coordinate.longitude,
|
||||||
@@ -1170,9 +1279,7 @@ struct MapHomeView: View {
|
|||||||
"intentID": String(intent.id),
|
"intentID": String(intent.id),
|
||||||
"intent选点revision": String(intent.selectionRevision),
|
"intent选点revision": String(intent.selectionRevision),
|
||||||
"当前选点revision": String(mapState.selection.revision),
|
"当前选点revision": String(mapState.selection.revision),
|
||||||
"修正前地图标准": previousMapCoordinateSystem.rawValue,
|
"App已确认地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
|
||||||
"修正后地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
|
|
||||||
"地图标准发生修正": String(mapCoordinateSystemChange != nil),
|
|
||||||
"accepted": String(accepted),
|
"accepted": String(accepted),
|
||||||
"显示坐标字段": CoordinateConverter.currentMapCoordinateSystem.rawValue,
|
"显示坐标字段": CoordinateConverter.currentMapCoordinateSystem.rawValue,
|
||||||
"持久化字段": "WGS-84+GCJ-02"
|
"持久化字段": "WGS-84+GCJ-02"
|
||||||
|
|||||||
@@ -222,6 +222,13 @@ final class MapLocationState: ObservableObject {
|
|||||||
if coordinate == nil { realtimeLocation = nil }
|
if coordinate == nil { realtimeLocation = nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Discards a blue-point sample represented in a superseded MapKit
|
||||||
|
/// coordinate system. The next native callback repopulates the cache.
|
||||||
|
func clearRealtimeLocationForMapCoordinateSystemChange() {
|
||||||
|
realtimeLocation = nil
|
||||||
|
realtimeCoordinate = nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateExplicitName(_ name: String, forFavoriteID favoriteID: UUID) {
|
func updateExplicitName(_ name: String, forFavoriteID favoriteID: UUID) {
|
||||||
guard selection.source == .favorite(favoriteID) else { return }
|
guard selection.source == .favorite(favoriteID) else { return }
|
||||||
selection = MapSelection(
|
selection = MapSelection(
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ enum CoordinateConverter {
|
|||||||
let current: MapCoordinateSystem
|
let current: MapCoordinateSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RuntimeMapCoordinateSystemRefreshResult: Equatable {
|
||||||
|
case unchanged(MapCoordinateSystem)
|
||||||
|
case changed(MapCoordinateSystemChange)
|
||||||
|
case unavailable(reason: String)
|
||||||
|
case cancelled
|
||||||
|
}
|
||||||
|
|
||||||
/// 当前 Apple 地图坐标标准。检测不可用时使用国内 GCJ-02 作为兜底。
|
/// 当前 Apple 地图坐标标准。检测不可用时使用国内 GCJ-02 作为兜底。
|
||||||
@MainActor static var currentMapCoordinateSystem = MapCoordinateSystem.gcj02
|
@MainActor static var currentMapCoordinateSystem = MapCoordinateSystem.gcj02
|
||||||
@MainActor private static var mapCoordinateSystemCheckPending = false
|
@MainActor private static var mapCoordinateSystemCheckPending = false
|
||||||
@@ -110,7 +117,7 @@ enum CoordinateConverter {
|
|||||||
let nextType: MapCoordinateSystem
|
let nextType: MapCoordinateSystem
|
||||||
switch await fixedAnchorCoordinateSystemProbe() {
|
switch await fixedAnchorCoordinateSystemProbe() {
|
||||||
case let .response(name, count):
|
case let .response(name, count):
|
||||||
nextType = mapCoordinateSystem(anchorName: name)
|
nextType = mapCoordinateSystem(forFixedAnchorFirstResultName: name)
|
||||||
initialMapCoordinateSystemUsedFallback = false
|
initialMapCoordinateSystemUsedFallback = false
|
||||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测获得明确结果", details: [
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测获得明确结果", details: [
|
||||||
"首条名称": name,
|
"首条名称": name,
|
||||||
@@ -146,7 +153,7 @@ enum CoordinateConverter {
|
|||||||
return currentMapCoordinateSystem
|
return currentMapCoordinateSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = applyDetectedMapCoordinateSystem(nextType)
|
currentMapCoordinateSystem = nextType
|
||||||
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准已确定,允许创建地图", details: [
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准已确定,允许创建地图", details: [
|
||||||
"最终标准": nextType.rawValue,
|
"最终标准": nextType.rawValue,
|
||||||
"使用兜底": String(initialMapCoordinateSystemUsedFallback),
|
"使用兜底": String(initialMapCoordinateSystemUsedFallback),
|
||||||
@@ -155,64 +162,77 @@ enum CoordinateConverter {
|
|||||||
return nextType
|
return nextType
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-runs the same location-independent fixed-anchor probe after a
|
/// Re-runs the fixed-anchor MapKit behavior probe while the map is alive.
|
||||||
/// spoofing environment change. Runtime callers must never infer MapKit's
|
/// Runtime failures preserve the last confirmed type: a potentially spoofed
|
||||||
/// representation from the geographic region of the current location: that
|
/// Core Location sample is not authoritative for MapKit's representation.
|
||||||
/// sample may itself already be virtual.
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func refreshMapCoordinateSystem() async -> MapCoordinateSystemChange? {
|
static func refreshRuntimeMapCoordinateSystem(reason: String) async -> RuntimeMapCoordinateSystemRefreshResult {
|
||||||
guard !mapCoordinateSystemCheckPending else {
|
guard !mapCoordinateSystemCheckPending else {
|
||||||
RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新已在进行中")
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测合并到进行中请求", details: [
|
||||||
return nil
|
"触发原因": reason,
|
||||||
|
"当前标准": currentMapCoordinateSystem.rawValue
|
||||||
|
])
|
||||||
|
return .unchanged(currentMapCoordinateSystem)
|
||||||
}
|
}
|
||||||
mapCoordinateSystemCheckPending = true
|
mapCoordinateSystemCheckPending = true
|
||||||
defer { mapCoordinateSystemCheckPending = false }
|
defer { mapCoordinateSystemCheckPending = false }
|
||||||
|
|
||||||
RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新开始", details: [
|
let previous = currentMapCoordinateSystem
|
||||||
"当前标准": currentMapCoordinateSystem.rawValue,
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测开始", details: [
|
||||||
"锚点": "22.283819,114.158439"
|
"触发原因": reason,
|
||||||
|
"检测前标准": previous.rawValue,
|
||||||
|
"锚点": "22.283819,114.158439",
|
||||||
|
"缓存": "false"
|
||||||
])
|
])
|
||||||
|
|
||||||
switch await fixedAnchorCoordinateSystemProbe() {
|
switch await fixedAnchorCoordinateSystemProbe() {
|
||||||
case let .response(name, count):
|
case let .response(name, count):
|
||||||
let detected = mapCoordinateSystem(anchorName: name)
|
guard !Task.isCancelled else {
|
||||||
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测结果已过期,取消写入", details: [
|
||||||
|
"触发原因": reason,
|
||||||
|
"保留标准": previous.rawValue
|
||||||
|
])
|
||||||
|
return .cancelled
|
||||||
|
}
|
||||||
|
let detected = mapCoordinateSystem(forFixedAnchorFirstResultName: name)
|
||||||
initialMapCoordinateSystemUsedFallback = false
|
initialMapCoordinateSystemUsedFallback = false
|
||||||
let change = applyDetectedMapCoordinateSystem(detected)
|
guard detected != previous else {
|
||||||
RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新完成", details: [
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测完成,标准未变化", details: [
|
||||||
|
"触发原因": reason,
|
||||||
"首条名称": name,
|
"首条名称": name,
|
||||||
"结果数": String(count),
|
"结果数": String(count),
|
||||||
"探测标准": detected.rawValue,
|
"确认标准": detected.rawValue
|
||||||
"发生切换": String(change != nil)
|
|
||||||
])
|
])
|
||||||
return change
|
return .unchanged(detected)
|
||||||
case .unavailable(let reason), .timedOut(let reason):
|
|
||||||
RuntimeLogger.warning("APP", "坐标转换", "运行期地图坐标标准刷新不可用,保留当前标准", details: [
|
|
||||||
"原因": reason,
|
|
||||||
"当前标准": currentMapCoordinateSystem.rawValue
|
|
||||||
])
|
|
||||||
return nil
|
|
||||||
case .cancelled:
|
|
||||||
RuntimeLogger.info("APP", "坐标转换", "运行期地图坐标标准刷新被取消,保留当前标准", details: [
|
|
||||||
"当前标准": currentMapCoordinateSystem.rawValue
|
|
||||||
])
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
let change = MapCoordinateSystemChange(previous: previous, current: detected)
|
||||||
|
|
||||||
/// Applies a coordinate system that was resolved by the fixed-anchor
|
|
||||||
/// probe. Kept as a small deterministic seam for regression tests.
|
|
||||||
@MainActor
|
|
||||||
static func applyDetectedMapCoordinateSystem(_ detected: MapCoordinateSystem) -> MapCoordinateSystemChange? {
|
|
||||||
guard detected != currentMapCoordinateSystem else { return nil }
|
|
||||||
let change = MapCoordinateSystemChange(
|
|
||||||
previous: currentMapCoordinateSystem,
|
|
||||||
current: detected
|
|
||||||
)
|
|
||||||
currentMapCoordinateSystem = detected
|
currentMapCoordinateSystem = detected
|
||||||
return change
|
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准运行期检测发现切换", details: [
|
||||||
|
"触发原因": reason,
|
||||||
|
"首条名称": name,
|
||||||
|
"结果数": String(count),
|
||||||
|
"from": previous.rawValue,
|
||||||
|
"to": detected.rawValue
|
||||||
|
])
|
||||||
|
return .changed(change)
|
||||||
|
case .unavailable(let failureReason), .timedOut(let failureReason):
|
||||||
|
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准运行期检测失败,保留当前标准", details: [
|
||||||
|
"触发原因": reason,
|
||||||
|
"原因": failureReason,
|
||||||
|
"保留标准": previous.rawValue
|
||||||
|
])
|
||||||
|
return .unavailable(reason: failureReason)
|
||||||
|
case .cancelled:
|
||||||
|
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测已取消", details: [
|
||||||
|
"触发原因": reason,
|
||||||
|
"保留标准": previous.rawValue
|
||||||
|
])
|
||||||
|
return .cancelled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func mapCoordinateSystem(anchorName: String) -> MapCoordinateSystem {
|
static func mapCoordinateSystem(forFixedAnchorFirstResultName name: String) -> MapCoordinateSystem {
|
||||||
anchorName == "林士街" ? .gcj02 : .wgs84
|
name == "林士街" ? .gcj02 : .wgs84
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A user-requested realtime sample is WGS-84 and can correct a provisional
|
/// A user-requested realtime sample is WGS-84 and can correct a provisional
|
||||||
@@ -227,12 +247,14 @@ enum CoordinateConverter {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let next: MapCoordinateSystem = usesGCJ02ServiceArea(lat: coordinate.latitude, lon: coordinate.longitude) ? .gcj02 : .wgs84
|
let next: MapCoordinateSystem = usesGCJ02ServiceArea(lat: coordinate.latitude, lon: coordinate.longitude) ? .gcj02 : .wgs84
|
||||||
guard let change = applyDetectedMapCoordinateSystem(next) else {
|
guard next != currentMapCoordinateSystem else {
|
||||||
RuntimeLogger.info("APP", "坐标转换", "实时定位确认兜底地图坐标标准无需修正", details: [
|
RuntimeLogger.info("APP", "坐标转换", "实时定位确认兜底地图坐标标准无需修正", details: [
|
||||||
"当前标准": currentMapCoordinateSystem.rawValue
|
"当前标准": currentMapCoordinateSystem.rawValue
|
||||||
])
|
])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
let change = MapCoordinateSystemChange(previous: currentMapCoordinateSystem, current: next)
|
||||||
|
currentMapCoordinateSystem = next
|
||||||
RuntimeLogger.warning("APP", "坐标转换", "实时定位修正启动兜底地图坐标标准", details: [
|
RuntimeLogger.warning("APP", "坐标转换", "实时定位修正启动兜底地图坐标标准", details: [
|
||||||
"from": change.previous.rawValue,
|
"from": change.previous.rawValue,
|
||||||
"to": change.current.rawValue
|
"to": change.current.rawValue
|
||||||
|
|||||||
@@ -116,11 +116,24 @@ final class FavoriteLocationStore: ObservableObject {
|
|||||||
mapCoordinateSystem: CoordinateConverter.MapCoordinateSystem,
|
mapCoordinateSystem: CoordinateConverter.MapCoordinateSystem,
|
||||||
accuracy: Int
|
accuracy: Int
|
||||||
) -> FavoriteLocation {
|
) -> FavoriteLocation {
|
||||||
let favorite = FavoriteLocation(
|
save(
|
||||||
|
FavoriteLocation(
|
||||||
name: name,
|
name: name,
|
||||||
coordinatePair: .init(mapCoordinate: mapCoordinate, mapCoordinateSystem: mapCoordinateSystem),
|
coordinatePair: .init(mapCoordinate: mapCoordinate, mapCoordinateSystem: mapCoordinateSystem),
|
||||||
accuracy: accuracy
|
accuracy: accuracy
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves a coordinate pair whose source representation was already typed
|
||||||
|
/// before an asynchronous map-coordinate-system refresh.
|
||||||
|
@discardableResult
|
||||||
|
func save(name: String, coordinatePair: CoordinatePair, accuracy: Int) -> FavoriteLocation {
|
||||||
|
save(FavoriteLocation(name: name, coordinatePair: coordinatePair, accuracy: accuracy))
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func save(_ favorite: FavoriteLocation) -> FavoriteLocation {
|
||||||
favorites.removeAll {
|
favorites.removeAll {
|
||||||
abs($0.coordinatePair.wgs84.latitude - favorite.coordinatePair.wgs84.latitude) < 0.000001
|
abs($0.coordinatePair.wgs84.latitude - favorite.coordinatePair.wgs84.latitude) < 0.000001
|
||||||
&& abs($0.coordinatePair.wgs84.longitude - favorite.coordinatePair.wgs84.longitude) < 0.000001
|
&& abs($0.coordinatePair.wgs84.longitude - favorite.coordinatePair.wgs84.longitude) < 0.000001
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ final class FavoriteLocationStoreTests: XCTestCase {
|
|||||||
XCTAssertNotEqual(favorite.coordinatePair.gcj02.longitude, wgs.longitude)
|
XCTAssertNotEqual(favorite.coordinatePair.gcj02.longitude, wgs.longitude)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSavingPrecomputedPairDoesNotReinterpretItAfterMapTypeRefresh() {
|
||||||
|
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
|
defer { defaults.removePersistentDomain(forName: suite) }
|
||||||
|
let pair = CoordinateConverter.coordinatePair(
|
||||||
|
lat: 22.296_642,
|
||||||
|
lon: 114.172_175,
|
||||||
|
mapCoordinateSystem: .wgs84
|
||||||
|
)
|
||||||
|
|
||||||
|
let favorite = FavoriteLocationStore(defaults: defaults).save(
|
||||||
|
name: "香港天文台",
|
||||||
|
coordinatePair: pair,
|
||||||
|
accuracy: 25
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(favorite.coordinatePair, pair)
|
||||||
|
}
|
||||||
|
|
||||||
func testLegacyFavoriteIsUpgradedAsDomesticGCJAndRewritten() throws {
|
func testLegacyFavoriteIsUpgradedAsDomesticGCJAndRewritten() throws {
|
||||||
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
|
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
|
||||||
let defaults = UserDefaults(suiteName: suite)!
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
@@ -122,41 +141,15 @@ final class FavoriteLocationStoreTests: XCTestCase {
|
|||||||
XCTAssertNil(CoordinateConverter.diagnoseRepresentation(sample: unrelated, pair: pair).inferredSystem)
|
XCTAssertNil(CoordinateConverter.diagnoseRepresentation(sample: unrelated, pair: pair).inferredSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
func testFixedAnchorResultNameUsesOneSharedMapTypeRule() {
|
||||||
func testDetectedMapCoordinateSystemChangePreventsHongKongWGS84DoubleConversion() {
|
XCTAssertEqual(
|
||||||
let original = CoordinateConverter.currentMapCoordinateSystem
|
CoordinateConverter.mapCoordinateSystem(forFixedAnchorFirstResultName: "林士街"),
|
||||||
defer { _ = CoordinateConverter.applyDetectedMapCoordinateSystem(original) }
|
.gcj02
|
||||||
_ = CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02)
|
|
||||||
|
|
||||||
let hongKongObservatory = CLLocationCoordinate2D(
|
|
||||||
latitude: 22.302_344,
|
|
||||||
longitude: 114.174_566
|
|
||||||
)
|
)
|
||||||
let stalePair = CoordinatePair(
|
XCTAssertEqual(
|
||||||
mapCoordinate: hongKongObservatory,
|
CoordinateConverter.mapCoordinateSystem(forFixedAnchorFirstResultName: "Connaught Road West"),
|
||||||
mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem
|
.wgs84
|
||||||
)
|
)
|
||||||
|
|
||||||
let change = CoordinateConverter.applyDetectedMapCoordinateSystem(.wgs84)
|
|
||||||
let pair = CoordinatePair(
|
|
||||||
mapCoordinate: hongKongObservatory,
|
|
||||||
mapCoordinateSystem: CoordinateConverter.currentMapCoordinateSystem
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(change?.previous, .gcj02)
|
|
||||||
XCTAssertEqual(change?.current, .wgs84)
|
|
||||||
XCTAssertNotEqual(stalePair.wgs84.longitude, hongKongObservatory.longitude)
|
|
||||||
XCTAssertEqual(pair.wgs84.latitude, hongKongObservatory.latitude, accuracy: 0.000_000_1)
|
|
||||||
XCTAssertEqual(pair.wgs84.longitude, hongKongObservatory.longitude, accuracy: 0.000_000_1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testApplyingSameDetectedMapCoordinateSystemDoesNotReportAChange() {
|
|
||||||
let original = CoordinateConverter.currentMapCoordinateSystem
|
|
||||||
defer { _ = CoordinateConverter.applyDetectedMapCoordinateSystem(original) }
|
|
||||||
_ = CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02)
|
|
||||||
|
|
||||||
XCTAssertNil(CoordinateConverter.applyDetectedMapCoordinateSystem(.gcj02))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMapConfigurationNeverRequestsRealUserLocation() {
|
func testMapConfigurationNeverRequestsRealUserLocation() {
|
||||||
|
|||||||
@@ -173,6 +173,19 @@ final class MapLocationStateTests: XCTestCase {
|
|||||||
XCTAssertEqual(state.selection.source, .search)
|
XCTAssertEqual(state.selection.source, .search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testMapCoordinateSystemChangeClearsSupersededRealtimeSampleWithoutMovingSelection() {
|
||||||
|
let state = MapLocationState(initialCoordinate: initial)
|
||||||
|
state.selectSearchResult(.init(latitude: 31.23, longitude: 121.47), name: "外滩")
|
||||||
|
let selection = state.selection
|
||||||
|
state.updateRealtimeLocation(CLLocation(latitude: 30.42, longitude: 114.25))
|
||||||
|
|
||||||
|
state.clearRealtimeLocationForMapCoordinateSystemChange()
|
||||||
|
|
||||||
|
XCTAssertNil(state.realtimeLocation)
|
||||||
|
XCTAssertNil(state.realtimeCoordinate)
|
||||||
|
XCTAssertEqual(state.selection, selection)
|
||||||
|
}
|
||||||
|
|
||||||
func testRealtimeIntentCanImmediatelyAcceptNativeLocation() {
|
func testRealtimeIntentCanImmediatelyAcceptNativeLocation() {
|
||||||
let state = MapLocationState(initialCoordinate: initial)
|
let state = MapLocationState(initialCoordinate: initial)
|
||||||
let nativeLocation = CLLocation(latitude: 30.42, longitude: 114.25)
|
let nativeLocation = CLLocation(latitude: 30.42, longitude: 114.25)
|
||||||
|
|||||||
@@ -65,12 +65,19 @@ if grep -q 'logEvent("CONNECT " + host + " -> passthrough")' "$ROOT/Core/proxy.g
|
|||||||
fi
|
fi
|
||||||
grep -q 'enum SystemSettingsNavigator' "$SETTINGS_NAVIGATOR" || fail "shared settings navigator is missing"
|
grep -q 'enum SystemSettingsNavigator' "$SETTINGS_NAVIGATOR" || fail "shared settings navigator is missing"
|
||||||
grep -q 'await CoordinateConverter.resolveInitialMapCoordinateSystem()' "$CONTENT" || fail "map type must resolve before MapHomeView construction"
|
grep -q 'await CoordinateConverter.resolveInitialMapCoordinateSystem()' "$CONTENT" || fail "map type must resolve before MapHomeView construction"
|
||||||
grep -q 'static func refreshMapCoordinateSystem() async' "$CONVERTER" || fail "runtime MapKit coordinate-system changes must reuse the fixed-anchor probe"
|
grep -q 'refreshRuntimeMapCoordinateSystem(reason:' "$CONVERTER" || fail "fixed-anchor map type must support runtime refresh"
|
||||||
grep -q 'fixedAnchorCoordinateSystemProbe()' "$CONVERTER" || fail "runtime coordinate-system refresh must use the location-independent fixed anchor"
|
grep -q 'scheduleBluePointMapCoordinateSystemRefresh()' "$MAP_HOME" || fail "native blue-point samples must trigger runtime map-type refresh while spoofing"
|
||||||
test "$(grep -c 'initialMapCoordinateSystemUsedFallback = false' "$CONVERTER")" -ge 2 || fail "a successful runtime anchor probe must disable location-region fallback correction"
|
! grep -A3 'private func scheduleBluePointMapCoordinateSystemRefresh' "$MAP_HOME" | grep -q 'spoofState == .active' || fail "blue-point map-type refresh must also detect the return to physical location"
|
||||||
grep -q 'refreshMapCoordinateSystemAfterLocationEnvironmentChange' "$MAP_HOME" || fail "MapHomeView must refresh the coordinate system after spoofing changes the location environment"
|
grep -q 'awaitCoordinatedMapCoordinateSystemRefresh(reason: "点击实时定位")' "$MAP_HOME" || fail "realtime button must await the coordinated map-type refresh"
|
||||||
grep -q 'nearestTargetDistance' "$MAP_HOME" || fail "spoof refresh must wait until the MapKit blue point reaches the active target"
|
grep -q 'awaitCoordinatedMapCoordinateSystemRefresh(reason: "保存收藏")' "$MAP_HOME" || fail "favorite save must await the coordinated map-type refresh"
|
||||||
grep -q 'reprojectMapSelection(for: change)' "$MAP_HOME" || fail "coordinate-system refresh must replay the persisted coordinate pair"
|
grep -q 'awaitCoordinatedMapCoordinateSystemRefresh(reason: "App回到前台")' "$MAP_HOME" || fail "foreground recovery must reuse the coordinated map-type refresh"
|
||||||
|
grep -q '地图坐标标准运行期检测结果已过期,取消写入' "$CONVERTER" || fail "cancelled runtime probes must not mutate the global map type"
|
||||||
|
! grep -q 'source == .coreLocation.*correctMapCoordinateSystemUsingRealtime' "$MAP_HOME" || fail "runtime Core Location samples must not infer MapKit type"
|
||||||
|
grep -q 'clearRealtimeLocationForMapCoordinateSystemChange' "$MAP_HOME" || fail "map-type changes must discard superseded blue-point samples"
|
||||||
|
grep -q '地图坐标类型已变化' "$MAP_HOME" || fail "map-type changes must emit an explicit searchable business log"
|
||||||
|
grep -q '图钉已按新类型重设' "$MAP_HOME" || fail "map-type change log must report pin reprojection"
|
||||||
|
grep -q '国内标准 GCJ-02' "$MAP_HOME" || fail "current selection panel must show GCJ-02"
|
||||||
|
grep -q '国际标准 WGS-84' "$MAP_HOME" || fail "current selection panel must show WGS-84"
|
||||||
grep -q 'phase = .map' "$CONTENT" || fail "ContentView must explicitly gate MapHomeView construction"
|
grep -q 'phase = .map' "$CONTENT" || fail "ContentView must explicitly gate MapHomeView construction"
|
||||||
! grep -q 'startTileProbe' "$MAP_HOME" || fail "MapHomeView must not start a second fixed-anchor coordinate-system probe"
|
! grep -q 'startTileProbe' "$MAP_HOME" || fail "MapHomeView must not start a second fixed-anchor coordinate-system probe"
|
||||||
! grep -q 'initializeMap()' "$MAP_HOME" || fail "MapHomeView must not replay a second map initialization from onAppear"
|
! grep -q 'initializeMap()' "$MAP_HOME" || fail "MapHomeView must not replay a second map initialization from onAppear"
|
||||||
|
|||||||
Reference in New Issue
Block a user