498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
126 lines
4.3 KiB
Swift
126 lines
4.3 KiB
Swift
// AIMapNavigation.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Builds one turn-by-turn URL. Provider order is Amap (高德) → Baidu →
|
|
// Apple Maps. The host calls `canOpenURL` and opens the URL itself.
|
|
|
|
import Foundation
|
|
|
|
public enum AIMapProvider: String, Equatable, Sendable {
|
|
case amap
|
|
case baidu
|
|
case apple
|
|
}
|
|
|
|
public enum AIMapNavigation: Sendable {
|
|
public static let sourceApplication = "OSGKeyboard"
|
|
public static let baiduSource = "ios.osgkeyboard"
|
|
/// Amap's usual current-location label when we have no coordinates.
|
|
public static let amapCurrentLocationName = "我的位置"
|
|
|
|
public static func provider(canOpen: (URL) -> Bool) -> AIMapProvider {
|
|
if canOpen(probe("iosamap")) || canOpen(probe("amapuri")) {
|
|
return .amap
|
|
}
|
|
if canOpen(probe("baidumap")) {
|
|
return .baidu
|
|
}
|
|
return .apple
|
|
}
|
|
|
|
public static func url(for route: AIMapRoute, canOpen: (URL) -> Bool) -> URL {
|
|
switch provider(canOpen: canOpen) {
|
|
case .amap:
|
|
if canOpen(probe("iosamap")) {
|
|
return amapURL(route, useLegacyScheme: true)
|
|
}
|
|
return amapURL(route, useLegacyScheme: false)
|
|
case .baidu:
|
|
return baiduURL(route)
|
|
case .apple:
|
|
return appleURL(route)
|
|
}
|
|
}
|
|
|
|
/// Keyboard payload is `origin|destination`. Host turns it into one URL.
|
|
public static func shortcutInput(
|
|
from encoded: String,
|
|
canOpen: (URL) -> Bool
|
|
) -> String? {
|
|
guard let route = AIAddressExtraction.route(from: encoded) else { return nil }
|
|
return url(for: route, canOpen: canOpen).absoluteString
|
|
}
|
|
|
|
// MARK: - Providers
|
|
|
|
private static func amapURL(_ route: AIMapRoute, useLegacyScheme: Bool) -> URL {
|
|
let originName = route.origin ?? amapCurrentLocationName
|
|
let items = [
|
|
URLQueryItem(name: "sourceApplication", value: sourceApplication),
|
|
URLQueryItem(name: "sname", value: originName),
|
|
URLQueryItem(name: "dname", value: route.destination),
|
|
URLQueryItem(name: "dev", value: "0"),
|
|
URLQueryItem(name: "t", value: "0")
|
|
]
|
|
if useLegacyScheme {
|
|
return makeURL(scheme: "iosamap", host: "path", path: nil, items: items)
|
|
}
|
|
let url = makeURL(scheme: "amapuri", host: "route", path: "/plan/", items: items)
|
|
// URLComponents drops the trailing slash; Amap's documented path is `/plan/`.
|
|
let withSlash = url.absoluteString.replacingOccurrences(
|
|
of: "://route/plan?",
|
|
with: "://route/plan/?"
|
|
)
|
|
return URL(string: withSlash) ?? url
|
|
}
|
|
|
|
private static func baiduURL(_ route: AIMapRoute) -> URL {
|
|
var items = [
|
|
URLQueryItem(name: "destination", value: "name:\(route.destination)"),
|
|
URLQueryItem(name: "mode", value: "driving"),
|
|
URLQueryItem(name: "src", value: baiduSource)
|
|
]
|
|
if let origin = route.origin {
|
|
items.insert(
|
|
URLQueryItem(name: "origin", value: "name:\(origin)"),
|
|
at: 0
|
|
)
|
|
}
|
|
return makeURL(scheme: "baidumap", host: "map", path: "/direction", items: items)
|
|
}
|
|
|
|
private static func appleURL(_ route: AIMapRoute) -> URL {
|
|
var items = [
|
|
URLQueryItem(name: "daddr", value: route.destination),
|
|
URLQueryItem(name: "dirflg", value: "d")
|
|
]
|
|
if let origin = route.origin {
|
|
items.insert(URLQueryItem(name: "saddr", value: origin), at: 0)
|
|
}
|
|
// `URLComponents(string: "maps://")` keeps the `://` that opening needs.
|
|
var components = URLComponents(string: "maps://")!
|
|
components.queryItems = items
|
|
return components.url ?? URL(string: "maps://")!
|
|
}
|
|
|
|
private static func probe(_ scheme: String) -> URL {
|
|
URL(string: "\(scheme)://")!
|
|
}
|
|
|
|
private static func makeURL(
|
|
scheme: String,
|
|
host: String?,
|
|
path: String?,
|
|
items: [URLQueryItem]
|
|
) -> URL {
|
|
var components = URLComponents()
|
|
components.scheme = scheme
|
|
components.host = host
|
|
if let path {
|
|
components.path = path
|
|
}
|
|
components.queryItems = items
|
|
return components.url ?? URL(string: "\(scheme)://")!
|
|
}
|
|
}
|