chore: add build artifacts, map assets, and routing data
@@ -11,7 +11,7 @@
|
||||
<application
|
||||
android:label="tactical_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
package com.intaleq.tactical_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val valhallaBridge = ValhallaBridge(this)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
valhallaBridge.register(flutterEngine)
|
||||
}
|
||||
|
||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
// تحرير الغراف المفتوح في الذاكرة عند تدمير محرك Flutter.
|
||||
valhallaBridge.release()
|
||||
super.cleanUpFlutterEngine(flutterEngine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.intaleq.tactical_app
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.valhalla.api.models.RouteRequest
|
||||
import com.valhalla.api.models.RouteResponse
|
||||
import com.valhalla.config.ValhallaConfigBuilder
|
||||
import com.valhalla.valhalla.Valhalla
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* جسر MethodChannel إلى محرك التوجيه Valhalla المحلي (valhalla-mobile).
|
||||
*
|
||||
* يدير حزمة توجيه الأردن (valhalla_tiles.tar + admins.sqlite) التي يُنزّلها
|
||||
* التطبيق من السيرفر، ويبني محرك Valhalla واحداً ويعيد استخدامه لكل الطلبات.
|
||||
* كل الحسابات تعمل 100% على الجهاز بدون أي اتصال شبكي.
|
||||
*/
|
||||
class ValhallaBridge(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "intaleq_tactical/valhalla"
|
||||
private const val TAG = "ValhallaBridge"
|
||||
}
|
||||
|
||||
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
|
||||
/** المحرك مُعاد استخدامه — بناؤه مكلف لأنه يفتح tar الغراف بالكامل. */
|
||||
private var engine: Valhalla? = null
|
||||
private var engineDir: String? = null
|
||||
|
||||
fun register(flutterEngine: FlutterEngine) {
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
// نلتقط Throwable وليس Exception فقط: فشل تحميل المكتبة الأصلية
|
||||
// (UnsatisfiedLinkError مثلاً) هو Error وليس Exception — التقاطه
|
||||
// يُظهر سبباً مقروءاً في Dart بدل انهيار التطبيق.
|
||||
"ensureReady" -> try {
|
||||
ensureReady(call.argument<String>("regionDir"))
|
||||
result.success(true)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "ensureReady failed", e)
|
||||
result.error("ENSURE_FAILED", describe(e), stackOf(e))
|
||||
}
|
||||
"route" -> try {
|
||||
val requestJson = call.argument<String>("request")
|
||||
?: return@setMethodCallHandler result.error("BAD_ARGS", "missing request", null)
|
||||
result.success(route(requestJson))
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "route failed", e)
|
||||
result.error("ROUTE_FAILED", describe(e), stackOf(e))
|
||||
}
|
||||
"release" -> try {
|
||||
release()
|
||||
result.success(true)
|
||||
} catch (e: Throwable) {
|
||||
result.error("RELEASE_FAILED", describe(e), stackOf(e))
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** يبني المحرك لمجلد الحزمة إن لم يكن جاهزاً، ويُعيد بناءه عند تغيّر المجلد. */
|
||||
@Synchronized
|
||||
private fun ensureReady(regionDirPath: String?) {
|
||||
val dir = File(regionDirPath ?: File(context.filesDir, "routing/jordan").absolutePath)
|
||||
if (!dir.exists()) {
|
||||
throw IllegalStateException("routing package dir does not exist: " + dir.absolutePath)
|
||||
}
|
||||
|
||||
val tilesTar = findFile(dir, "valhalla_tiles.tar")
|
||||
?: throw IllegalStateException("valhalla_tiles.tar missing in " + dir.absolutePath)
|
||||
|
||||
if (engine != null && engineDir == dir.absolutePath) return
|
||||
|
||||
release()
|
||||
|
||||
val config = ValhallaConfigBuilder().withTileExtract(tilesTar.absolutePath).build()
|
||||
engine = Valhalla(context, config)
|
||||
engineDir = dir.absolutePath
|
||||
Log.i(TAG, "Valhalla engine ready against " + tilesTar.absolutePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* حساب مسار: يستقبل طلب Valhalla JSON خاماً من Dart، يفكّه عبر Moshi إلى
|
||||
* RouteRequest النموذجي، ينفّذه محلياً ثم يعيد الاستجابة كنص JSON كامل
|
||||
* (trip/legs/maneuvers/shape + summary مع ascend/descend).
|
||||
*/
|
||||
@Synchronized
|
||||
private fun route(requestJson: String): String {
|
||||
ensureReady(engineDir)
|
||||
val activeEngine = engine ?: throw IllegalStateException("Valhalla engine not initialised")
|
||||
|
||||
val requestAdapter = moshi.adapter(RouteRequest::class.java)
|
||||
val request = requestAdapter.fromJson(requestJson)
|
||||
?: throw IllegalArgumentException("could not decode RouteRequest from Dart payload")
|
||||
|
||||
val response = activeEngine.route(request)
|
||||
|
||||
return when (response) {
|
||||
is com.valhalla.valhalla.ValhallaResponse.Json ->
|
||||
moshi.adapter(RouteResponse::class.java).toJson(response.jsonResponse)
|
||||
else ->
|
||||
throw IllegalArgumentException("only JSON route format is supported on-device")
|
||||
}
|
||||
}
|
||||
|
||||
/** تحرير المحرك الأصلي (يغلق tar الغراف المفتوح في الذاكرة). */
|
||||
@Synchronized
|
||||
fun release() {
|
||||
try {
|
||||
engine?.close()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "engine close failed", e)
|
||||
}
|
||||
engine = null
|
||||
engineDir = null
|
||||
}
|
||||
|
||||
/** بحث تكراري عن ملف داخل مجلد الحزمة (البنية قد تتغير بين إصدارات الحزمة). */
|
||||
private fun findFile(dir: File, name: String): File? {
|
||||
if (!dir.isDirectory) return null
|
||||
dir.listFiles()?.forEach { child ->
|
||||
if (child.isFile && child.name == name) return child
|
||||
if (child.isDirectory) findFile(child, name)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun describe(e: Throwable): String {
|
||||
val cause = e.cause?.let { " (${it.javaClass.simpleName}: ${it.message})" } ?: ""
|
||||
return "${e.javaClass.simpleName}: ${e.message}$cause"
|
||||
}
|
||||
|
||||
private fun stackOf(e: Throwable): String = Log.getStackTraceString(e)
|
||||
}
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 304 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#090E1A</color>
|
||||
</resources>
|
||||