// // ScenicSelectionModels.swift // suixinkan // import Foundation /// 景区列表展示标签,对齐 Android `openStatus`。 enum ScenicSelectionOpenStatus: Equatable { case nearest case closed case none } /// 景区选择列表项,对齐 Android `Sceniclistv2Entity` 展示字段。 struct ScenicSpotDisplayItem: Equatable, Hashable { let id: Int let name: String let coverImg: String let address: String let distanceText: String let openStatus: ScenicSelectionOpenStatus let isSelected: Bool var statusLabel: String? { switch openStatus { case .nearest: "距离最近" case .closed: "暂未营业" case .none: nil } } } /// 景区距离计算,对齐 Android Haversine 逻辑。 enum ScenicDistanceCalculator { private static let earthRadiusMeters = 6_371_000.0 static func distanceKilometers( fromLat: Double, fromLng: Double, toLat: Double, toLng: Double ) -> Double { let dLat = (toLat - fromLat) * .pi / 180 let dLng = (toLng - fromLng) * .pi / 180 let lat1 = fromLat * .pi / 180 let lat2 = toLat * .pi / 180 let a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1) * cos(lat2) * sin(dLng / 2) * sin(dLng / 2) let c = 2 * atan2(sqrt(a), sqrt(1 - a)) return (earthRadiusMeters * c) / 1000 } static func formatDistanceKilometers(_ kilometers: Double) -> String { if kilometers <= 0 || !kilometers.isFinite { return "0" } return String(format: "%.2f", kilometers) } } /// 将 role-permission 景区映射为选择页展示项。 enum ScenicSpotDisplayMapper { static func buildItems( scenicList: [ScenicInfo], searchQuery: String, currentLat: Double?, currentLng: Double?, selectedScenicId: Int ) -> [ScenicSpotDisplayItem] { let hasLocation = (currentLat ?? 0) > 0 && (currentLng ?? 0) > 0 let searchLower = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() struct ScenicWithDistance { let item: ScenicSpotDisplayItem let distance: Double } let mapped = scenicList.map { scenic -> ScenicWithDistance in let loc = scenic.location let distance: Double if let loc, hasLocation, let currentLat, let currentLng { distance = ScenicDistanceCalculator.distanceKilometers( fromLat: currentLat, fromLng: currentLng, toLat: loc.lat, toLng: loc.lng ) } else if loc == nil, hasLocation { distance = .greatestFiniteMagnitude } else { distance = 0 } let address = loc?.address.isEmpty == false ? (loc?.address ?? "--") : "--" let distanceText: String if loc == nil { distanceText = "--" } else if distance > 0, distance < .greatestFiniteMagnitude { distanceText = ScenicDistanceCalculator.formatDistanceKilometers(distance) } else { distanceText = "0" } let openStatus: ScenicSelectionOpenStatus = scenic.status == 2 ? .closed : .none let item = ScenicSpotDisplayItem( id: scenic.id, name: scenic.name, coverImg: scenic.coverImg ?? "", address: address, distanceText: distanceText, openStatus: openStatus, isSelected: selectedScenicId > 0 && scenic.id == selectedScenicId ) return ScenicWithDistance(item: item, distance: distance) } let filtered = mapped.filter { entry in guard !searchLower.isEmpty else { return true } return entry.item.name.lowercased().contains(searchLower) || entry.item.address.lowercased().contains(searchLower) } let sorted: [ScenicWithDistance] if hasLocation { sorted = filtered.sorted { $0.distance < $1.distance } } else { sorted = filtered } let baseItems = sorted.map(\.item) guard hasLocation, !baseItems.isEmpty else { return baseItems } return baseItems.enumerated().map { index, item in if index == 0, item.openStatus != .closed { return ScenicSpotDisplayItem( id: item.id, name: item.name, coverImg: item.coverImg, address: item.address, distanceText: item.distanceText, openStatus: .nearest, isSelected: item.isSelected ) } return item } } }