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 /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 } }