fix: refresh MapKit coordinate type at runtime

This commit is contained in:
xweiba
2026-08-07 14:13:25 +08:00
parent 7e362e6b6c
commit 8acc33186c
7 changed files with 371 additions and 57 deletions
+219 -52
View File
@@ -78,10 +78,15 @@ struct MapHomeView: View {
@State private var wifiChangeObserverToken: UUID?
@State private var wifiVerificationTask: Task<Void, Never>?
@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 locationOperationTask: Task<Void, Never>?
@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
@State private var activeSpoofLat: Double?
@State private var activeSpoofLon: Double?
@@ -232,7 +237,7 @@ struct MapHomeView: View {
.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)
@@ -279,6 +284,13 @@ struct MapHomeView: View {
wifiVerificationTask?.cancel()
wifiVerificationTask = nil
wifiVerificationID = nil
mapCoordinateSystemRefreshTask?.cancel()
mapCoordinateSystemRefreshTask = nil
bluePointRefreshPending = false
realtimeButtonTask?.cancel()
realtimeButtonTask = nil
favoriteSaveTask?.cancel()
favoriteSaveTask = nil
}
.onChange(of: proxy.isRunning) { running in
if runtimeMode.mode == .localWiFi, !running && spoofState == .active {
@@ -412,27 +424,8 @@ struct MapHomeView: View {
HStack {
VStack(alignment: .leading, spacing: 3) {
Text(mapState.displayName ?? "当前选点").font(.subheadline.weight(.semibold)).lineLimit(1)
Text(String(format: "%.6f, %.6f", mapState.selection.coordinate.latitude, mapState.selection.coordinate.longitude))
.font(.caption.monospaced())
.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)
}
}
coordinateRow(label: "国内标准 GCJ-02", system: .gcj02)
coordinateRow(label: "国际标准 WGS-84", system: .wgs84)
}
Spacer()
//
@@ -457,23 +450,7 @@ struct MapHomeView: View {
favorites.select(nil)
return
}
let snapshot = currentSelectionFavorite
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
)
saveCurrentSelectionAsFavorite()
} label: {
Image(systemName: favorites.selectedFavoriteID != nil ? "star.fill" : "star")
.font(.system(size: 18, weight: .semibold))
@@ -482,6 +459,7 @@ struct MapHomeView: View {
}
.buttonStyle(.plain)
.foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray)
.disabled(favoriteSaveTask != nil)
.accessibilityLabel(favorites.selectedFavoriteID != nil ? "已收藏,点击取消收藏" : "收藏当前选点")
}
//
@@ -761,6 +739,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 func startMapRuntimeOnce() {
@@ -774,19 +804,152 @@ struct MapHomeView: View {
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
// its input boundary. Replaying the matching stored representation
// avoids a second GCJ/WGS conversion and its accumulated offset.
guard let stored = LastCoordinateStore.load() else {
RuntimeLogger.warning("APP", "坐标转换", "地图坐标标准切换时未找到当前选点缓存")
return
return false
}
mapState.reprojectSelectionForMapCoordinateSystemChange(stored.coordinate(for: change.current))
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准切换后已使用缓存坐标对回显当前选点", details: [
"from": change.previous.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() {
@@ -916,6 +1079,18 @@ struct MapHomeView: View {
}
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()
RuntimeLogger.info("APP", "实时定位", "用户点击实时定位", details: [
"intentID": String(intent.id),
@@ -951,6 +1126,7 @@ struct MapHomeView: View {
private func handleNativeRealtimeLocation(_ location: CLLocation) {
mapState.updateRealtimeLocation(location)
logSpoofCoordinateDiagnosisIfNeeded(location)
scheduleBluePointMapCoordinateSystemRefresh()
guard let context = realtimeRequestContext else {
return
}
@@ -1079,14 +1255,7 @@ struct MapHomeView: View {
sourceDescription: String
) {
let currentViewport = mapState.viewportMeters
let previousMapCoordinateSystem = CoordinateConverter.currentMapCoordinateSystem
let sourceCoordinateSystem = source.coordinateSystem
let mapCoordinateSystemChange = source == .coreLocation
? CoordinateConverter.correctMapCoordinateSystemUsingRealtime(coordinate)
: nil
if let change = mapCoordinateSystemChange {
reprojectMapSelection(for: change)
}
let pair = CoordinateConverter.coordinatePair(
lat: coordinate.latitude,
lon: coordinate.longitude,
@@ -1103,9 +1272,7 @@ struct MapHomeView: View {
"intentID": String(intent.id),
"intent选点revision": String(intent.selectionRevision),
"当前选点revision": String(mapState.selection.revision),
"修正前地图标准": previousMapCoordinateSystem.rawValue,
"修正后地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
"地图标准发生修正": String(mapCoordinateSystemChange != nil),
"App已确认地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
"accepted": String(accepted),
"显示坐标字段": CoordinateConverter.currentMapCoordinateSystem.rawValue,
"持久化字段": "WGS-84+GCJ-02"
+7
View File
@@ -222,6 +222,13 @@ final class MapLocationState: ObservableObject {
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) {
guard selection.source == .favorite(favoriteID) else { return }
selection = MapSelection(
+74 -1
View File
@@ -85,6 +85,13 @@ enum CoordinateConverter {
let current: MapCoordinateSystem
}
enum RuntimeMapCoordinateSystemRefreshResult: Equatable {
case unchanged(MapCoordinateSystem)
case changed(MapCoordinateSystemChange)
case unavailable(reason: String)
case cancelled
}
/// Apple 使 GCJ-02
@MainActor static var currentMapCoordinateSystem = MapCoordinateSystem.gcj02
@MainActor private static var mapCoordinateSystemCheckPending = false
@@ -110,7 +117,7 @@ enum CoordinateConverter {
let nextType: MapCoordinateSystem
switch await fixedAnchorCoordinateSystemProbe() {
case let .response(name, count):
nextType = name == "林士街" ? .gcj02 : .wgs84
nextType = mapCoordinateSystem(forFixedAnchorFirstResultName: name)
initialMapCoordinateSystemUsedFallback = false
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准检测获得明确结果", details: [
"首条名称": name,
@@ -155,6 +162,72 @@ enum CoordinateConverter {
return nextType
}
/// Re-runs the fixed-anchor MapKit behavior probe while the map is alive.
/// Runtime failures preserve the last confirmed type: a potentially spoofed
/// Core Location sample is not authoritative for MapKit's representation.
@MainActor
static func refreshRuntimeMapCoordinateSystem(reason: String) async -> RuntimeMapCoordinateSystemRefreshResult {
guard !mapCoordinateSystemCheckPending else {
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测合并到进行中请求", details: [
"触发原因": reason,
"当前标准": currentMapCoordinateSystem.rawValue
])
return .unchanged(currentMapCoordinateSystem)
}
mapCoordinateSystemCheckPending = true
defer { mapCoordinateSystemCheckPending = false }
let previous = currentMapCoordinateSystem
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测开始", details: [
"触发原因": reason,
"检测前标准": previous.rawValue,
"锚点": "22.283819,114.158439",
"缓存": "false"
])
switch await fixedAnchorCoordinateSystemProbe() {
case let .response(name, count):
let detected = mapCoordinateSystem(forFixedAnchorFirstResultName: name)
initialMapCoordinateSystemUsedFallback = false
guard detected != previous else {
RuntimeLogger.info("APP", "坐标转换", "地图坐标标准运行期检测完成,标准未变化", details: [
"触发原因": reason,
"首条名称": name,
"结果数": String(count),
"确认标准": detected.rawValue
])
return .unchanged(detected)
}
let change = MapCoordinateSystemChange(previous: previous, current: detected)
currentMapCoordinateSystem = detected
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
}
}
static func mapCoordinateSystem(forFixedAnchorFirstResultName name: String) -> MapCoordinateSystem {
name == "林士街" ? .gcj02 : .wgs84
}
/// A user-requested realtime sample is WGS-84 and can correct a provisional
/// startup map coordinate system without altering persisted coordinate pairs.
@MainActor
+14 -1
View File
@@ -116,11 +116,24 @@ final class FavoriteLocationStore: ObservableObject {
mapCoordinateSystem: CoordinateConverter.MapCoordinateSystem,
accuracy: Int
) -> FavoriteLocation {
let favorite = FavoriteLocation(
save(
FavoriteLocation(
name: name,
coordinatePair: .init(mapCoordinate: mapCoordinate, mapCoordinateSystem: mapCoordinateSystem),
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 {
abs($0.coordinatePair.wgs84.latitude - favorite.coordinatePair.wgs84.latitude) < 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)
}
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 {
let suite = "FavoriteLocationStoreTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
@@ -122,6 +141,17 @@ final class FavoriteLocationStoreTests: XCTestCase {
XCTAssertNil(CoordinateConverter.diagnoseRepresentation(sample: unrelated, pair: pair).inferredSystem)
}
func testFixedAnchorResultNameUsesOneSharedMapTypeRule() {
XCTAssertEqual(
CoordinateConverter.mapCoordinateSystem(forFixedAnchorFirstResultName: "林士街"),
.gcj02
)
XCTAssertEqual(
CoordinateConverter.mapCoordinateSystem(forFixedAnchorFirstResultName: "Connaught Road West"),
.wgs84
)
}
func testMapConfigurationNeverRequestsRealUserLocation() {
XCTAssertFalse(MapConfiguration.default.showsUserLocation)
XCTAssertFalse(MapConfiguration.default.allowsCurrentLocationRequest)
@@ -173,6 +173,19 @@ final class MapLocationStateTests: XCTestCase {
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() {
let state = MapLocationState(initialCoordinate: initial)
let nativeLocation = CLLocation(latitude: 30.42, longitude: 114.25)
+11
View File
@@ -65,6 +65,17 @@ if grep -q 'logEvent("CONNECT " + host + " -> passthrough")' "$ROOT/Core/proxy.g
fi
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 'refreshRuntimeMapCoordinateSystem(reason:' "$CONVERTER" || fail "fixed-anchor map type must support runtime refresh"
grep -q 'scheduleBluePointMapCoordinateSystemRefresh()' "$MAP_HOME" || fail "native blue-point samples must trigger runtime map-type refresh while spoofing"
! 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 'awaitCoordinatedMapCoordinateSystemRefresh(reason: "点击实时定位")' "$MAP_HOME" || fail "realtime button must await the coordinated map-type refresh"
grep -q 'awaitCoordinatedMapCoordinateSystemRefresh(reason: "保存收藏")' "$MAP_HOME" || fail "favorite save must await the coordinated map-type refresh"
! 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 '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"