chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'offline_routing_engine.dart';
|
||||
|
||||
/// [ValhallaOfflineEngine] - محرك التوجيه المحلي السيادي (100% على الجهاز)
|
||||
///
|
||||
/// English:
|
||||
/// Dart bridge to the native valhalla-mobile routing engine (Android/iOS).
|
||||
/// Routes are calculated against a downloaded Jordan tile extract
|
||||
/// (valhalla_tiles.tar built from the same OSM data as the server GraphHopper,
|
||||
/// with SRTM elevation baked into every edge). Zero network usage.
|
||||
///
|
||||
/// العربية:
|
||||
/// جسر Dart إلى محرك Valhalla الأصلي على الجهاز. يحسب المسارات على شبكة الطرق
|
||||
/// الحقيقية للأردن من حزمة التوجيه المنزّلة (بنفس بيانات OSM وارتفاعات SRTM
|
||||
/// المستخدمة في GraphHopper على السيرفر) بدون أي اتصال شبكي.
|
||||
class ValhallaOfflineEngine {
|
||||
static const MethodChannel _channel = MethodChannel('intaleq_tactical/valhalla');
|
||||
|
||||
/// Warm up / (re)build the native engine against [regionDir] tiles.
|
||||
/// Returns true when the native library answered (engine usable).
|
||||
static Future<bool> ensureReady({String? regionDir}) async {
|
||||
try {
|
||||
// فحص مسبق محلي: لا نزعج الجسر الأصلي إذا لم تُثبَّت الحزمة أصلاً
|
||||
final dir = regionDir;
|
||||
if (dir != null && !Directory(dir).existsSync()) {
|
||||
debugPrint('ValhallaOfflineEngine: package not installed at $dir');
|
||||
return false;
|
||||
}
|
||||
await _channel.invokeMethod<bool>('ensureReady', {'regionDir': regionDir});
|
||||
return true;
|
||||
} on MissingPluginException {
|
||||
debugPrint('ValhallaOfflineEngine: native bridge not available on this platform');
|
||||
return false;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('ValhallaOfflineEngine ensureReady failed: ${e.code} ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('ValhallaOfflineEngine ensureReady error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> release() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('release');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Calculate a fully offline road-following route.
|
||||
///
|
||||
/// Returns null when the engine/package is unavailable so callers can fall
|
||||
/// back to the legacy synthetic graph.
|
||||
static Future<OfflineRoutePlan?> calculateOfflineRoute({
|
||||
required LatLng start,
|
||||
required LatLng destination,
|
||||
TacticalVehicleProfile profile = TacticalVehicleProfile.convoy,
|
||||
String? regionDir,
|
||||
}) async {
|
||||
try {
|
||||
final ready = await ensureReady(regionDir: regionDir);
|
||||
if (!ready) return null;
|
||||
|
||||
final request = jsonEncode({
|
||||
'locations': [
|
||||
{'lat': start.latitude, 'lon': start.longitude, 'type': 'break'},
|
||||
{'lat': destination.latitude, 'lon': destination.longitude, 'type': 'break'},
|
||||
],
|
||||
'costing': 'auto',
|
||||
'alternatives': false,
|
||||
'directions_options': {
|
||||
'units': 'kilometers',
|
||||
'language': 'ar',
|
||||
'directions_type': 'instructions',
|
||||
},
|
||||
'id': 'intaleq_tactical',
|
||||
});
|
||||
|
||||
final raw = await _channel.invokeMethod<String>('route', {'request': request});
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
|
||||
final data = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return _parseTripResponse(data, profile);
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('ValhallaOfflineEngine route failed: ${e.code} ${e.message}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('ValhallaOfflineEngine route error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response Parsing ─────────────────────────────────────────────────────
|
||||
|
||||
static OfflineRoutePlan? _parseTripResponse(Map<String, dynamic> data, TacticalVehicleProfile profile) {
|
||||
final trip = data['trip'];
|
||||
if (trip is! Map<String, dynamic>) return null;
|
||||
|
||||
final status = (trip['status'] as num?)?.toInt() ?? -1;
|
||||
if (status != 0) {
|
||||
debugPrint('Valhalla trip status $status: ${trip['status_message']}');
|
||||
return null;
|
||||
}
|
||||
|
||||
final legs = trip['legs'];
|
||||
if (legs is! List || legs.isEmpty) return null;
|
||||
|
||||
final allPoints = <LatLng>[];
|
||||
final maneuvers = <RouteManeuver>[];
|
||||
|
||||
for (final legDynamic in legs) {
|
||||
if (legDynamic is! Map<String, dynamic>) continue;
|
||||
|
||||
// Shape: Valhalla encoded polyline, precision 6, includes altitude when
|
||||
// the tiles were built with SRTM elevation.
|
||||
final shapeStr = legDynamic['shape'] as String?;
|
||||
if (shapeStr == null) continue;
|
||||
final legPoints = decodePolyline6(shapeStr);
|
||||
final legOffset = allPoints.isEmpty ? 0 : allPoints.length - 1; // join at shared point
|
||||
|
||||
allPoints.addAll(legOffset == 0 ? legPoints : legPoints.skip(1));
|
||||
|
||||
// Maneuvers with real street names + Arabic narrative instructions.
|
||||
final legManeuvers = legDynamic['maneuvers'];
|
||||
if (legManeuvers is List) {
|
||||
for (final m in legManeuvers) {
|
||||
if (m is! Map<String, dynamic>) continue;
|
||||
final beginIdx = (m['begin_shape_index'] as num?)?.toInt() ?? 0;
|
||||
final globalIdx = (legOffset + beginIdx).clamp(0, allPoints.length - 1);
|
||||
|
||||
final streetNames = <String>[];
|
||||
final rawStreets = m['street_names'];
|
||||
if (rawStreets is List) {
|
||||
for (final s in rawStreets) {
|
||||
if (s is String && s.isNotEmpty) streetNames.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
maneuvers.add(RouteManeuver(
|
||||
instructionAr: (m['instruction'] as String?) ?? 'واصل السير',
|
||||
type: (m['type'] as num?)?.toInt() ?? 0,
|
||||
streetNames: streetNames,
|
||||
lengthKm: (m['length'] as num?)?.toDouble() ?? 0.0,
|
||||
location: allPoints[globalIdx],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allPoints.length < 2) return null;
|
||||
|
||||
final summary = trip['summary'];
|
||||
double totalKm = 0.0;
|
||||
double totalTimeSec = 0.0;
|
||||
double ascend = 0.0;
|
||||
double descend = 0.0;
|
||||
if (summary is Map<String, dynamic>) {
|
||||
totalKm = (summary['length'] as num?)?.toDouble() ?? 0.0;
|
||||
totalTimeSec = (summary['time'] as num?)?.toDouble() ?? 0.0;
|
||||
ascend = (summary['ascend'] as num?)?.toDouble() ?? 0.0;
|
||||
descend = (summary['descend'] as num?)?.toDouble() ?? 0.0;
|
||||
}
|
||||
if (totalKm <= 0) {
|
||||
totalKm = _haversineKm(allPoints.first, allPoints.last);
|
||||
}
|
||||
|
||||
// نفس معاملات التعديل التكتيكي المستخدمة في مسار السيرفر للحفاظ على الاتساق
|
||||
double profileFactor;
|
||||
switch (profile) {
|
||||
case TacticalVehicleProfile.convoy:
|
||||
profileFactor = 1.45;
|
||||
break;
|
||||
case TacticalVehicleProfile.armored:
|
||||
profileFactor = 1.75;
|
||||
break;
|
||||
case TacticalVehicleProfile.offroad4x4:
|
||||
profileFactor = 1.1;
|
||||
break;
|
||||
case TacticalVehicleProfile.rapidResponse:
|
||||
profileFactor = 0.85;
|
||||
break;
|
||||
}
|
||||
|
||||
return OfflineRoutePlan(
|
||||
polylinePoints: allPoints,
|
||||
totalDistanceKm: totalKm,
|
||||
estimatedDurationMinutes: (totalTimeSec / 60.0) * profileFactor,
|
||||
profile: profile,
|
||||
tacticalWaypoints: const ['موقع الانطلاق', 'الهدف التكتيكي المحدد'],
|
||||
isOffline: true,
|
||||
elevationGainMeters: ascend.round(),
|
||||
elevationLossMeters: descend.round(),
|
||||
usesRealRoadNetwork: true,
|
||||
maneuvers: maneuvers,
|
||||
);
|
||||
}
|
||||
|
||||
/// Decode Valhalla encoded polyline (precision 1e6, optional 3rd dim = altitude cm).
|
||||
static List<LatLng> decodePolyline6(String encoded) {
|
||||
final points = <LatLng>[];
|
||||
int index = 0;
|
||||
int lat = 0;
|
||||
int lng = 0;
|
||||
|
||||
int decodeChunk() {
|
||||
int result = 0;
|
||||
int shift = 0;
|
||||
int b;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20 && index < encoded.length);
|
||||
return (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||
}
|
||||
|
||||
while (index < encoded.length) {
|
||||
lat += decodeChunk();
|
||||
lng += decodeChunk();
|
||||
points.add(LatLng(lat / 1e6, lng / 1e6));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
static double _haversineKm(LatLng a, LatLng b) {
|
||||
const r = 6371.0;
|
||||
final dLat = (b.latitude - a.latitude) * (math.pi / 180.0);
|
||||
final dLng = (b.longitude - a.longitude) * (math.pi / 180.0);
|
||||
final la1 = a.latitude * (math.pi / 180.0);
|
||||
final la2 = b.latitude * (math.pi / 180.0);
|
||||
final h = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(la1) * math.cos(la2) * math.sin(dLng / 2) * math.sin(dLng / 2);
|
||||
return 2 * r * math.asin(math.sqrt(h));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user