chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
#if canImport(Valhalla)
|
||||
import Valhalla
|
||||
import ValhallaConfigModels
|
||||
#endif
|
||||
|
||||
/// ============================================================================
|
||||
/// [ValhallaBridge] - جسر التوجيه المحلي لمحرك Valhalla على iOS
|
||||
/// ============================================================================
|
||||
/// English:
|
||||
/// Swift bridge connecting Flutter's MethodChannel (intaleq_tactical/valhalla)
|
||||
/// to the native Valhalla offline routing engine on iOS.
|
||||
/// The Jordan tile extract (valhalla_tiles.tar) downloaded by
|
||||
/// OfflineRoutingPackageService into <Documents>/routing/jordan is opened by
|
||||
/// the C++ engine through ValhallaConfig(tileExtractTar:), and every route
|
||||
/// request runs 100% on-device with zero network usage.
|
||||
///
|
||||
/// العربية:
|
||||
/// جسر سويفت يربط قناة فلاتر بمحرك Valhalla الأصلي على iOS. يفتح المحرك ملف
|
||||
/// غراف طرق الأردن المنزّل من مجلد الحزمة ويحسب المسارات محلياً بالكامل مع
|
||||
/// ارتفاعات SRTM وأسماء الشوارع الحقيقية وتعليمات انعطاف عربية.
|
||||
/// ============================================================================
|
||||
class ValhallaBridge: NSObject {
|
||||
static let channelName = "intaleq_tactical/valhalla"
|
||||
|
||||
/// Serial queue guarding engine lifecycle + routing calls (C++ actor is not thread-safe).
|
||||
private let engineQueue = DispatchQueue(label: "com.intaleq.tactical.valhalla")
|
||||
|
||||
#if canImport(Valhalla)
|
||||
private var engine: Valhalla?
|
||||
private var engineDir: String?
|
||||
#endif
|
||||
|
||||
func register(with messenger: FlutterBinaryMessenger) {
|
||||
let channel = FlutterMethodChannel(name: ValhallaBridge.channelName, binaryMessenger: messenger)
|
||||
channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
|
||||
guard let self = self else { return }
|
||||
switch call.method {
|
||||
case "ensureReady":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let regionDir = args?["regionDir"] as? String
|
||||
self.handleEnsureReady(regionDirPath: regionDir, result: result)
|
||||
|
||||
case "route":
|
||||
let args = call.arguments as? [String: Any]
|
||||
guard let requestJson = args?["request"] as? String else {
|
||||
result(FlutterError(code: "BAD_ARGS", message: "Missing request JSON", details: nil))
|
||||
return
|
||||
}
|
||||
self.handleRoute(requestJson: requestJson, result: result)
|
||||
|
||||
case "release":
|
||||
self.releaseEngine()
|
||||
result(true)
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ensureReady
|
||||
|
||||
private func handleEnsureReady(regionDirPath: String?, result: @escaping FlutterResult) {
|
||||
let dir = Self.resolveRegionDir(regionDirPath)
|
||||
let tarPath = Self.findTar(named: "valhalla_tiles.tar", in: dir)?.path
|
||||
|
||||
guard let tarPath = tarPath else {
|
||||
result(FlutterError(
|
||||
code: "DIR_NOT_FOUND",
|
||||
message: "Routing package not installed at \(dir.path) — download it from the server while online",
|
||||
details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
#if canImport(Valhalla)
|
||||
engineQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// إعادة الاستخدام إن كان المحرك مبنياً على نفس الحزمة
|
||||
if let engine = self.engine, self.engineDir == dir.path {
|
||||
result(true)
|
||||
return
|
||||
}
|
||||
self.releaseEngineLocked()
|
||||
|
||||
do {
|
||||
let config = try ValhallaConfig(tileExtractTar: URL(fileURLWithPath: tarPath))
|
||||
self.engine = try Valhalla(config)
|
||||
self.engineDir = dir.path
|
||||
NSLog("[ValhallaBridge] engine ready against \(tarPath)")
|
||||
DispatchQueue.main.async { result(true) }
|
||||
} catch {
|
||||
NSLog("[ValhallaBridge] engine init failed: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
result(FlutterError(code: "ENGINE_INIT_FAILED",
|
||||
message: "\(error)",
|
||||
details: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
result(FlutterError(
|
||||
code: "FALLBACK_SYNTHETIC",
|
||||
message: "Valhalla SPM package is not linked into this build target",
|
||||
details: nil))
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - route
|
||||
|
||||
private func handleRoute(requestJson: String, result: @escaping FlutterResult) {
|
||||
#if canImport(Valhalla)
|
||||
engineQueue.async { [weak self] in
|
||||
guard let self = self, let engine = self.engine else {
|
||||
DispatchQueue.main.async {
|
||||
result(FlutterError(code: "NOT_READY",
|
||||
message: "Call ensureReady before route",
|
||||
details: nil))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// route(rawRequest:) يعيد JSON خاماً؛ أخطاء المحرك تعود داخل الجسم
|
||||
// بصيغة {"code": int, "message": str} ويفكّها طرف Dart مباشرة.
|
||||
let rawResponse = engine.route(rawRequest: requestJson)
|
||||
DispatchQueue.main.async { result(rawResponse) }
|
||||
}
|
||||
#else
|
||||
result(FlutterError(
|
||||
code: "FALLBACK_SYNTHETIC",
|
||||
message: "Valhalla SPM package is not linked into this build target",
|
||||
details: nil))
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - release
|
||||
|
||||
func releaseEngine() {
|
||||
#if canImport(Valhalla)
|
||||
engineQueue.async { [weak self] in
|
||||
self?.releaseEngineLocked()
|
||||
}
|
||||
#else
|
||||
// no-op
|
||||
#endif
|
||||
}
|
||||
|
||||
#if canImport(Valhalla)
|
||||
/// Must be called on engineQueue.
|
||||
private func releaseEngineLocked() {
|
||||
engine = nil
|
||||
engineDir = nil
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func resolveRegionDir(_ regionDirPath: String?) -> URL {
|
||||
if let path = regionDirPath {
|
||||
return URL(fileURLWithPath: path, isDirectory: true)
|
||||
}
|
||||
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
return docs.appendingPathComponent("routing/jordan", isDirectory: true)
|
||||
}
|
||||
|
||||
/// بحث تكراري عن ملف الغراف داخل مجلد الحزمة (البنية قد تختلف بين الإصدارات)
|
||||
private static func findTar(named name: String, in dir: URL) -> URL? {
|
||||
let fm = FileManager.default
|
||||
var isDir: ObjCBool = false
|
||||
guard fm.fileExists(atPath: dir.path, isDirectory: &isDir), isDir.boolValue else {
|
||||
return nil
|
||||
}
|
||||
if fm.fileExists(atPath: dir.appendingPathComponent(name).path) {
|
||||
return dir.appendingPathComponent(name)
|
||||
}
|
||||
guard let children = try? fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: [.isDirectoryKey]) else {
|
||||
return nil
|
||||
}
|
||||
for child in children {
|
||||
let isChildDir = (try? child.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
if isChildDir, let found = findTar(named: name, in: child) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user