605 lines
24 KiB
Swift
605 lines
24 KiB
Swift
//
|
||
// WildPhotographerReportMockStore.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import CoreLocation
|
||
import Foundation
|
||
import MapKit
|
||
|
||
/// 举报摄影师 Mock 数据仓库,统一管理演示种子数据和地图标记构建。
|
||
enum WildPhotographerReportMockStore {
|
||
static let fallbackLocation = "黄山风景区 · 光明顶景区"
|
||
|
||
static let initialImages = [
|
||
WildReportAttachment(kind: .image, title: "现场图片1")
|
||
]
|
||
|
||
static let initialVideos = [
|
||
WildReportAttachment(kind: .video, title: "现场视频1")
|
||
]
|
||
|
||
static let seedRiskPoints = [
|
||
WildReportRiskPoint(
|
||
id: 1,
|
||
title: "光明顶观景台附近",
|
||
location: "黄山风景区 · 光明顶景区",
|
||
distance: "120m",
|
||
riskLevel: "高",
|
||
evidenceCount: 6,
|
||
latestTime: "10分钟前",
|
||
description: "多名游客反馈有疑似未备案摄影师主动揽客,集中在观景台东侧步道附近。"
|
||
),
|
||
WildReportRiskPoint(
|
||
id: 2,
|
||
title: "迎客松游客出口",
|
||
location: "黄山风景区 · 玉屏景区",
|
||
distance: "860m",
|
||
riskLevel: "中",
|
||
evidenceCount: 3,
|
||
latestTime: "32分钟前",
|
||
description: "疑似人员以低价加急出片为话术引导游客线下付款,建议现场核查。"
|
||
),
|
||
WildReportRiskPoint(
|
||
id: 3,
|
||
title: "云谷索道排队区",
|
||
location: "黄山风景区 · 云谷景区",
|
||
distance: "1.4km",
|
||
riskLevel: "低",
|
||
evidenceCount: 2,
|
||
latestTime: "1小时前",
|
||
description: "排队区附近存在疑似私下拉客行为,已收到图片和位置线索。"
|
||
)
|
||
]
|
||
|
||
static let seedReports = [
|
||
WildReportRecord(
|
||
id: "JB20260618001",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||
status: .processing,
|
||
location: "那拉提景区 · 空中草原入口附近",
|
||
submitTime: "2026-06-18 10:24",
|
||
description: "疑似人员在观景台附近主动向游客售卖拍摄服务,并引导线下转账。",
|
||
wechatID: "wx_photographer_01",
|
||
phoneNumber: "13812345678",
|
||
imageCount: 3,
|
||
videoCount: 1,
|
||
handlerInfo: nil
|
||
),
|
||
WildReportRecord(
|
||
id: "JB20260617008",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||
status: .processed,
|
||
location: "那拉提景区 · 河谷草原游客中心",
|
||
submitTime: "2026-06-17 16:42",
|
||
description: "现场发现疑似未备案摄影师持续向游客推销旅拍套餐。",
|
||
wechatID: "nalati_photo",
|
||
phoneNumber: "",
|
||
imageCount: 2,
|
||
videoCount: 1,
|
||
handlerInfo: processedHandlerInfo(
|
||
handlerName: "赵强",
|
||
handlerPhone: "15938267120",
|
||
processingAt: "2026-06-17 17:10",
|
||
processedAt: "2026-06-17 18:26",
|
||
processOpinion: "已到场核查并完成劝离,处理结果已同步景区公安。",
|
||
imageSeedPrefix: "handler-jb008"
|
||
)
|
||
),
|
||
WildReportRecord(
|
||
id: "JB20260616003",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 3, label: "未戴工牌"),
|
||
status: .pending,
|
||
location: "那拉提景区 · 盘龙古道停车区",
|
||
submitTime: "2026-06-16 09:15",
|
||
description: "停车区附近有摄影师未佩戴工牌开展拍摄服务,已收到图片和位置线索。",
|
||
wechatID: "",
|
||
phoneNumber: "13987654321",
|
||
imageCount: 1,
|
||
videoCount: 0,
|
||
handlerInfo: nil
|
||
),
|
||
WildReportRecord(
|
||
id: "JB20260615002",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 4, label: "其他"),
|
||
status: .processed,
|
||
location: "那拉提景区 · 雪莲谷步道",
|
||
submitTime: "2026-06-15 14:30",
|
||
description: "举报线索经核查未发现异常摄影师服务行为,已归档。",
|
||
wechatID: "grassland_cam",
|
||
phoneNumber: "13600001111",
|
||
imageCount: 1,
|
||
videoCount: 0,
|
||
handlerInfo: processedHandlerInfo(
|
||
handlerName: "李娜",
|
||
handlerPhone: "18674598210",
|
||
processingAt: "2026-06-15 15:05",
|
||
processedAt: "2026-06-15 16:55",
|
||
processOpinion: "现场核查未发现未备案摄影师,已归档处理。",
|
||
imageSeedPrefix: "handler-jb002",
|
||
includeVideo: false
|
||
)
|
||
)
|
||
]
|
||
|
||
static let seedSupplementaryEvidences: [String: [WildReportSupplementaryEvidence]] = [
|
||
"JB20260618001": [
|
||
WildReportSupplementaryEvidence(
|
||
id: "SE001",
|
||
submitTime: "2026-06-18 14:20",
|
||
text: "补充现场视频,对方仍在揽客。",
|
||
imageCount: 1,
|
||
videoCount: 1
|
||
)
|
||
]
|
||
]
|
||
|
||
static let reportDateFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "yyyyMMdd"
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
return formatter
|
||
}()
|
||
|
||
static let submitTimeFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
return formatter
|
||
}()
|
||
|
||
static let nalatiCenter = CLLocationCoordinate2D(latitude: 43.382, longitude: 84.032)
|
||
|
||
private static let clueID1 = "2026年6月22日-0001"
|
||
private static let clueID2 = "2026年6月22日-0002"
|
||
private static let clueID3 = "2026年6月22日-0003"
|
||
private static let initialProcessingClueIDs: Set<String> = [clueID2]
|
||
|
||
/// 构建已处理举报的处理人信息 Mock 数据。
|
||
static func processedHandlerInfo(
|
||
handlerName: String,
|
||
handlerPhone: String,
|
||
processingAt: String,
|
||
processedAt: String,
|
||
processOpinion: String,
|
||
imageSeedPrefix: String,
|
||
includeVideo: Bool = true
|
||
) -> WildReportHandlerInfo {
|
||
var media: [WildReportCompletionMediaItem] = [
|
||
WildReportCompletionMediaItem(
|
||
id: "\(imageSeedPrefix)-img-1",
|
||
kind: .image,
|
||
coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-1/640/480",
|
||
duration: nil
|
||
),
|
||
WildReportCompletionMediaItem(
|
||
id: "\(imageSeedPrefix)-img-2",
|
||
kind: .image,
|
||
coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-2/640/480",
|
||
duration: nil
|
||
)
|
||
]
|
||
if includeVideo {
|
||
media.append(
|
||
WildReportCompletionMediaItem(
|
||
id: "\(imageSeedPrefix)-vid-1",
|
||
kind: .video,
|
||
coverURL: "https://picsum.photos/seed/\(imageSeedPrefix)-video/640/480",
|
||
duration: "00:18"
|
||
)
|
||
)
|
||
}
|
||
return WildReportHandlerInfo(
|
||
handlerName: handlerName,
|
||
handlerPhone: handlerPhone,
|
||
processingAt: processingAt,
|
||
processedAt: processedAt,
|
||
processOpinion: processOpinion,
|
||
completionMedia: media
|
||
)
|
||
}
|
||
|
||
/// 从已有举报编号中提取最大序号,供新编号递增。
|
||
static func maxSequence(from reports: [WildReportRecord]) -> Int {
|
||
reports.compactMap { record in
|
||
guard record.id.count >= 3 else { return nil }
|
||
return Int(record.id.suffix(3))
|
||
}.max() ?? 0
|
||
}
|
||
|
||
/// 解析联系方式,区分微信号和手机号。
|
||
static func parseContact(_ contact: String?) -> (wechat: String, phone: String) {
|
||
guard let contact else { return ("", "") }
|
||
let trimmed = contact.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty else { return ("", "") }
|
||
|
||
let digits = trimmed.filter(\.isNumber)
|
||
if digits.count >= 7, trimmed.filter({ $0.isNumber || "+-() ".contains($0) }).count == trimmed.count {
|
||
return ("", trimmed)
|
||
}
|
||
return (trimmed, "")
|
||
}
|
||
|
||
/// 根据线索状态返回地图标记类型。
|
||
static func markerKind(for clue: WildReportRadarActiveClue) -> WildReportMapMarkerKind {
|
||
switch clue.type {
|
||
case .blue:
|
||
return .selfLocation
|
||
case .green:
|
||
return .peerPhotographer
|
||
case .red:
|
||
if clue.processingStarted && !clue.completed {
|
||
return .processingClue
|
||
}
|
||
return .wildPhotographer
|
||
}
|
||
}
|
||
|
||
/// 构建风险地图全部标记点。
|
||
static func buildMarkers(record: WildReportRecord, riskPoints: [WildReportRiskPoint]) -> [WildReportMapMarker] {
|
||
_ = record
|
||
_ = riskPoints
|
||
|
||
let blueCoordinate = CLLocationCoordinate2D(latitude: 43.3779, longitude: 84.0217)
|
||
let redSeeds: [(id: String, name: String, coordinate: CLLocationCoordinate2D, clue: WildReportRadarActiveClue)] = [
|
||
redSeed(
|
||
id: clueID1,
|
||
name: "空中草原观景道",
|
||
coordinate: CLLocationCoordinate2D(latitude: 43.4068, longitude: 84.0582),
|
||
updatedAt: "5分钟前",
|
||
distanceText: "320米",
|
||
reporterName: "张三",
|
||
reporterPhone: "13333333333",
|
||
contents: [
|
||
"多人结伴揽客,疑似无资质摄影团队,携带专业器材在观景道附近活动。",
|
||
"补充证据:对方仍在向游客推销拍照套餐,并拒绝出示相关资质。"
|
||
],
|
||
imageSeeds: ["nalati-radar-1", "nalati-radar-2", "nalati-radar-3"]
|
||
),
|
||
redSeed(
|
||
id: clueID2,
|
||
name: "塔吾萨尼步道",
|
||
coordinate: CLLocationCoordinate2D(latitude: 43.3652, longitude: 84.0385),
|
||
updatedAt: "18分钟前",
|
||
distanceText: "780米",
|
||
reporterName: "李四",
|
||
reporterPhone: "18674598210",
|
||
contents: [
|
||
"疑似二次转移,仍在塔吾萨尼步道附近揽客拍照。",
|
||
"补充:对方穿黑色外套,携带三脚架,在步道入口反复搭讪游客。"
|
||
],
|
||
imageSeeds: ["nalati-radar-4", "nalati-radar-5"]
|
||
),
|
||
redSeed(
|
||
id: clueID3,
|
||
name: "河谷草原西南侧",
|
||
coordinate: CLLocationCoordinate2D(latitude: 43.3546, longitude: 84.0136),
|
||
updatedAt: "昨天17:45",
|
||
distanceText: "910米",
|
||
reporterName: "王五",
|
||
reporterPhone: "15938267120",
|
||
contents: ["游客反馈有人在步道旁揽客拍照,行为较为可疑。"],
|
||
imageSeeds: ["nalati-radar-8"]
|
||
)
|
||
]
|
||
|
||
let nearestText = nearestAbnormalText(
|
||
blueCoordinate: blueCoordinate,
|
||
redCoordinates: redSeeds.map(\.coordinate)
|
||
)
|
||
|
||
var markers: [WildReportMapMarker] = [
|
||
WildReportMapMarker(
|
||
id: "ME",
|
||
title: "河谷草原入口",
|
||
coordinate: blueCoordinate,
|
||
kind: .selfLocation,
|
||
detail: .activeClue(
|
||
WildReportRadarActiveClue(
|
||
id: "ME",
|
||
type: .blue,
|
||
displayTitle: "当前位置:河谷草原入口",
|
||
statusText: "当前位置",
|
||
actionText: "刷新定位",
|
||
nearestAbnormalText: nearestText,
|
||
distance: "0 米",
|
||
direction: "蓝色基准点",
|
||
updatedAt: "实时定位",
|
||
name: "河谷草原入口",
|
||
storeName: nil,
|
||
avatar: nil,
|
||
summary: "蓝色标记为当前位置,可对比周边摄影师风险点位距离。",
|
||
detail: "当前定位在河谷草原入口附近,建议优先关注 800 米内的异常点位。",
|
||
distanceText: nil,
|
||
reporterName: nil,
|
||
reporterPhone: nil,
|
||
maskedReporterPhone: nil,
|
||
reportContents: [],
|
||
images: [],
|
||
processingStarted: false,
|
||
completed: false,
|
||
record: nil,
|
||
processingTimeline: [],
|
||
completionPhoto: nil
|
||
)
|
||
)
|
||
)
|
||
]
|
||
|
||
for seed in redSeeds {
|
||
let processingStarted = initialProcessingClueIDs.contains(seed.id)
|
||
let clue = enrichClue(seed.clue, processingStarted: processingStarted)
|
||
markers.append(
|
||
WildReportMapMarker(
|
||
id: seed.id,
|
||
title: seed.name,
|
||
coordinate: seed.coordinate,
|
||
kind: markerKind(for: clue),
|
||
detail: .activeClue(clue)
|
||
)
|
||
)
|
||
}
|
||
|
||
markers.append(contentsOf: greenMarkers())
|
||
return markers
|
||
}
|
||
|
||
/// 根据全部标记计算地图可视区域。
|
||
static func region(for markers: [WildReportMapMarker]) -> MKCoordinateRegion {
|
||
guard let first = markers.first else {
|
||
return MKCoordinateRegion(
|
||
center: nalatiCenter,
|
||
span: MKCoordinateSpan(latitudeDelta: 0.025, longitudeDelta: 0.025)
|
||
)
|
||
}
|
||
|
||
var minLat = first.coordinate.latitude
|
||
var maxLat = first.coordinate.latitude
|
||
var minLon = first.coordinate.longitude
|
||
var maxLon = first.coordinate.longitude
|
||
|
||
for marker in markers {
|
||
minLat = min(minLat, marker.coordinate.latitude)
|
||
maxLat = max(maxLat, marker.coordinate.latitude)
|
||
minLon = min(minLon, marker.coordinate.longitude)
|
||
maxLon = max(maxLon, marker.coordinate.longitude)
|
||
}
|
||
|
||
let center = CLLocationCoordinate2D(
|
||
latitude: (minLat + maxLat) / 2,
|
||
longitude: (minLon + maxLon) / 2
|
||
)
|
||
let span = MKCoordinateSpan(
|
||
latitudeDelta: max(0.022, (maxLat - minLat) * 1.8),
|
||
longitudeDelta: max(0.022, (maxLon - minLon) * 1.8)
|
||
)
|
||
return MKCoordinateRegion(center: center, span: span)
|
||
}
|
||
|
||
private static func redSeed(
|
||
id: String,
|
||
name: String,
|
||
coordinate: CLLocationCoordinate2D,
|
||
updatedAt: String,
|
||
distanceText: String,
|
||
reporterName: String,
|
||
reporterPhone: String,
|
||
contents: [String],
|
||
imageSeeds: [String]
|
||
) -> (id: String, name: String, coordinate: CLLocationCoordinate2D, clue: WildReportRadarActiveClue) {
|
||
let reportContents = contents.enumerated().map { index, content in
|
||
WildReportRadarReportContent(time: index == 0 ? "14:32:18" : "14:45:06", content: content)
|
||
}
|
||
return (
|
||
id,
|
||
name,
|
||
coordinate,
|
||
WildReportRadarActiveClue(
|
||
id: id,
|
||
type: .red,
|
||
displayTitle: "线索 ID:\(id)",
|
||
statusText: "疑似打野",
|
||
actionText: "导航到此",
|
||
nearestAbnormalText: nil,
|
||
distance: nil,
|
||
direction: nil,
|
||
updatedAt: updatedAt,
|
||
name: name,
|
||
storeName: nil,
|
||
avatar: nil,
|
||
summary: nil,
|
||
detail: nil,
|
||
distanceText: distanceText,
|
||
reporterName: reporterName,
|
||
reporterPhone: reporterPhone,
|
||
maskedReporterPhone: maskPhone(reporterPhone),
|
||
reportContents: reportContents,
|
||
images: imageSeeds.map { "https://picsum.photos/seed/\($0)/640/480" },
|
||
processingStarted: false,
|
||
completed: false,
|
||
record: nil,
|
||
processingTimeline: [],
|
||
completionPhoto: nil
|
||
)
|
||
)
|
||
}
|
||
|
||
private static func greenMarkers() -> [WildReportMapMarker] {
|
||
let seeds: [(id: String, coordinate: CLLocationCoordinate2D, name: String, store: String, detail: String)] = [
|
||
("PG20240618001", CLLocationCoordinate2D(latitude: 43.4042, longitude: 84.0186), "阿依达娜", "空中草原旅拍店", "当前在空中草原区域服务游客拍摄。"),
|
||
("PG20240618002", CLLocationCoordinate2D(latitude: 43.3985, longitude: 84.0408), "波拉提", "天界台摄影服务点", "当前在天界台附近接待预约游客。"),
|
||
("PG20240618003", CLLocationCoordinate2D(latitude: 43.3828, longitude: 84.0794), "米热丽", "塔吾萨尼影像馆", "当前在塔吾萨尼步道附近提供旅拍服务。")
|
||
]
|
||
|
||
return seeds.map { seed in
|
||
let clue = WildReportRadarActiveClue(
|
||
id: seed.id,
|
||
type: .green,
|
||
displayTitle: "认证摄影师",
|
||
statusText: "认证摄影师",
|
||
actionText: "导航到此",
|
||
nearestAbnormalText: nil,
|
||
distance: "430 米",
|
||
direction: "景区内",
|
||
updatedAt: "在线",
|
||
name: seed.name,
|
||
storeName: seed.store,
|
||
avatar: "person.crop.circle.fill",
|
||
summary: "绿色标记为已认证摄影师,可与红色摄影师风险点位区分。",
|
||
detail: seed.detail,
|
||
distanceText: nil,
|
||
reporterName: nil,
|
||
reporterPhone: nil,
|
||
maskedReporterPhone: nil,
|
||
reportContents: [],
|
||
images: [],
|
||
processingStarted: false,
|
||
completed: false,
|
||
record: nil,
|
||
processingTimeline: [],
|
||
completionPhoto: nil
|
||
)
|
||
return WildReportMapMarker(
|
||
id: seed.id,
|
||
title: seed.name,
|
||
coordinate: seed.coordinate,
|
||
kind: .peerPhotographer,
|
||
detail: .activeClue(clue)
|
||
)
|
||
}
|
||
}
|
||
|
||
private static func processRecord(for clueID: String) -> WildReportRadarProcessRecord? {
|
||
switch clueID {
|
||
case clueID1:
|
||
return WildReportRadarProcessRecord(
|
||
handlerName: "王磊",
|
||
phone: "13812346721",
|
||
maskedPhone: maskPhone("13812346721"),
|
||
statusText: "待处理",
|
||
processResult: "待现场核查,请前往空中草原观景道确认摄影师线索。",
|
||
processedAt: nil,
|
||
processingAt: nil
|
||
)
|
||
case clueID2:
|
||
return WildReportRadarProcessRecord(
|
||
handlerName: "李娜",
|
||
phone: "18674598210",
|
||
maskedPhone: maskPhone("18674598210"),
|
||
statusText: "处理中",
|
||
processResult: "待现场核查,请前往塔吾萨尼步道确认摄影师线索。",
|
||
processedAt: nil,
|
||
processingAt: "2024-05-18 10:35"
|
||
)
|
||
case clueID3:
|
||
return WildReportRadarProcessRecord(
|
||
handlerName: "赵强",
|
||
phone: "15938267120",
|
||
maskedPhone: maskPhone("15938267120"),
|
||
statusText: "已处理",
|
||
processResult: "已到场核查并完成劝离,处理结果已同步景区公安。",
|
||
processedAt: "2024-05-17 18:26",
|
||
processingAt: "2024-05-17 17:45"
|
||
)
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
private static func buildProcessingTimeline(
|
||
record: WildReportRadarProcessRecord,
|
||
completed: Bool
|
||
) -> [WildReportRadarProcessingTimelineItem] {
|
||
let startedAt = record.processingAt ?? "2024-05-18 10:35"
|
||
return [
|
||
WildReportRadarProcessingTimelineItem(title: "处理时间", time: startedAt, status: "done"),
|
||
WildReportRadarProcessingTimelineItem(
|
||
title: "处理完成时间",
|
||
time: completed ? (record.processedAt ?? "刚刚") : "待完成",
|
||
status: completed ? "done" : "current"
|
||
)
|
||
]
|
||
}
|
||
|
||
private static func enrichClue(
|
||
_ base: WildReportRadarActiveClue,
|
||
processingStarted: Bool,
|
||
completed: Bool = false,
|
||
completionPhoto: String? = nil
|
||
) -> WildReportRadarActiveClue {
|
||
let record = processRecord(for: base.id)
|
||
let isProcessing = processingStarted && !completed
|
||
let timeline = (isProcessing || completed) && record != nil
|
||
? buildProcessingTimeline(record: record!, completed: completed)
|
||
: []
|
||
|
||
return WildReportRadarActiveClue(
|
||
id: base.id,
|
||
type: base.type,
|
||
displayTitle: base.displayTitle,
|
||
statusText: completed ? "已处理" : (isProcessing ? "正在处理中" : base.statusText),
|
||
actionText: base.actionText,
|
||
nearestAbnormalText: base.nearestAbnormalText,
|
||
distance: base.distance,
|
||
direction: base.direction,
|
||
updatedAt: completed ? (record?.processedAt ?? "刚刚") : base.updatedAt,
|
||
name: base.name,
|
||
storeName: base.storeName,
|
||
avatar: base.avatar,
|
||
summary: base.summary,
|
||
detail: base.detail,
|
||
distanceText: base.distanceText,
|
||
reporterName: base.reporterName,
|
||
reporterPhone: base.reporterPhone,
|
||
maskedReporterPhone: base.maskedReporterPhone,
|
||
reportContents: base.reportContents,
|
||
images: base.images,
|
||
processingStarted: processingStarted || completed,
|
||
completed: completed,
|
||
record: (isProcessing || completed) ? record : nil,
|
||
processingTimeline: timeline,
|
||
completionPhoto: completionPhoto
|
||
)
|
||
}
|
||
|
||
private static func maskPhone(_ phone: String) -> String {
|
||
guard phone.count == 11, phone.allSatisfy(\.isNumber) else { return phone }
|
||
return "\(phone.prefix(3))****\(phone.suffix(4))"
|
||
}
|
||
|
||
private static func nearestAbnormalText(
|
||
blueCoordinate: CLLocationCoordinate2D,
|
||
redCoordinates: [CLLocationCoordinate2D]
|
||
) -> String {
|
||
guard !redCoordinates.isEmpty else { return "距离最近的异常点位0.00KM" }
|
||
let minKm = redCoordinates.map {
|
||
calcDistanceKm(
|
||
lat1: blueCoordinate.latitude,
|
||
lon1: blueCoordinate.longitude,
|
||
lat2: $0.latitude,
|
||
lon2: $0.longitude
|
||
)
|
||
}.min() ?? 0
|
||
return String(format: "距离最近的异常点位%.2fKM", minKm)
|
||
}
|
||
|
||
private static func calcDistanceKm(
|
||
lat1: Double,
|
||
lon1: Double,
|
||
lat2: Double,
|
||
lon2: Double
|
||
) -> Double {
|
||
let earthRadiusKm = 6371.0
|
||
let toRad = { (deg: Double) in deg * .pi / 180 }
|
||
let dLat = toRad(lat2 - lat1)
|
||
let dLon = toRad(lon2 - lon1)
|
||
let a = sin(dLat / 2) * sin(dLat / 2)
|
||
+ cos(toRad(lat1)) * cos(toRad(lat2)) * sin(dLon / 2) * sin(dLon / 2)
|
||
return earthRadiusKm * 2 * atan2(sqrt(a), sqrt(1 - a))
|
||
}
|
||
}
|