189 lines
7.6 KiB
Dart
189 lines
7.6 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../config/app_config.dart';
|
|
import 'landmark_database.dart';
|
|
import 'offline_routing_engine.dart';
|
|
import 'offline_routing_package_service.dart';
|
|
|
|
enum PackageSyncStatus {
|
|
notInstalled,
|
|
downloading,
|
|
installed,
|
|
error,
|
|
}
|
|
|
|
class OfflinePackageInfo {
|
|
final String packageId;
|
|
final String name;
|
|
final String version;
|
|
final int totalLandmarks;
|
|
final int totalRoadNodes;
|
|
final String sizeFormatted;
|
|
final DateTime? lastSyncTime;
|
|
final PackageSyncStatus status;
|
|
final bool canSyncNow;
|
|
final int daysUntilNextSync;
|
|
|
|
/// هل غراف الطرق الحقيقي (Valhalla) مثبت على الجهاز؟
|
|
final bool realRoadRoutingReady;
|
|
|
|
const OfflinePackageInfo({
|
|
required this.packageId,
|
|
required this.name,
|
|
required this.version,
|
|
required this.totalLandmarks,
|
|
required this.totalRoadNodes,
|
|
required this.sizeFormatted,
|
|
this.lastSyncTime,
|
|
this.status = PackageSyncStatus.notInstalled,
|
|
this.canSyncNow = true,
|
|
this.daysUntilNextSync = 0,
|
|
this.realRoadRoutingReady = false,
|
|
});
|
|
}
|
|
|
|
class OfflinePackageManager {
|
|
static const String _pkgKey = 'offline_jordan_pkg_v2';
|
|
static const int syncCooldownDays = 14; // Cooldown enabled: updates allowed every 14 days
|
|
static String get defaultServerUrl => AppConfig.serverUrl;
|
|
static String get defaultApiKey => AppConfig.apiKey;
|
|
|
|
static ValueNotifier<double> syncProgress = ValueNotifier(0.0);
|
|
static ValueNotifier<String> syncStatusMessage = ValueNotifier('جاهز للمزامنة');
|
|
static ValueNotifier<PackageSyncStatus> packageStatus = ValueNotifier(PackageSyncStatus.installed);
|
|
|
|
/// Reset sync lock and preferences / تصفير قفل المزامنة بالكامل
|
|
static Future<void> resetSyncLock() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('${_pkgKey}_date');
|
|
await prefs.remove('${_pkgKey}_synced');
|
|
}
|
|
|
|
static Future<OfflinePackageInfo> getPackageInfo() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final syncDateStr = prefs.getString('${_pkgKey}_date');
|
|
final isSynced = prefs.getBool('${_pkgKey}_synced') ?? true;
|
|
final lastDate = syncDateStr != null ? DateTime.tryParse(syncDateStr) : null;
|
|
|
|
bool canSync = true;
|
|
int remainingDays = 0;
|
|
|
|
if (lastDate != null) {
|
|
final daysSinceSync = DateTime.now().difference(lastDate).inDays;
|
|
if (daysSinceSync < syncCooldownDays) {
|
|
canSync = false;
|
|
remainingDays = syncCooldownDays - daysSinceSync;
|
|
}
|
|
}
|
|
|
|
// حالة حزمة التوجيه الحقيقية (شبكة الطرق الكاملة)
|
|
final routingInstalled = await OfflineRoutingPackageService.isInstalled();
|
|
final routingVersion = await OfflineRoutingPackageService.installedVersion();
|
|
|
|
return OfflinePackageInfo(
|
|
packageId: 'jordan-tactical-offline-v2',
|
|
name: 'حزمة الأردن التكتيكية الميدانية الكاملة (Jordan Tactical Pack)',
|
|
version: routingInstalled ? (routingVersion ?? '2.4.0') : '2.4.0',
|
|
totalLandmarks: LandmarkDatabase.allLandmarks.length,
|
|
totalRoadNodes: routingInstalled ? 450000 : 36,
|
|
sizeFormatted: routingInstalled
|
|
? 'غراف طرق الأردن الحقيقي + SRTM مثبت محلياً'
|
|
: '3.5 MB (شبكة الطرق) + 25 MB (المعالم)',
|
|
lastSyncTime: lastDate,
|
|
status: isSynced ? PackageSyncStatus.installed : PackageSyncStatus.notInstalled,
|
|
canSyncNow: canSync,
|
|
daysUntilNextSync: remainingDays,
|
|
realRoadRoutingReady: routingInstalled,
|
|
);
|
|
}
|
|
|
|
/// Download and sync full Jordan tactical data package from server with 14-day throttle protection
|
|
static Future<bool> downloadFullPackage({
|
|
String? serverUrl,
|
|
String? apiKey,
|
|
bool force = false,
|
|
}) async {
|
|
if (!force) {
|
|
final info = await getPackageInfo();
|
|
if (!info.canSyncNow && info.status == PackageSyncStatus.installed) {
|
|
debugPrint('Package sync skipped (14-day cooldown active. ${info.daysUntilNextSync} days left).');
|
|
return true; // Already synced and in cooldown
|
|
}
|
|
}
|
|
|
|
final activeUrl = serverUrl ?? defaultServerUrl;
|
|
final activeKey = apiKey ?? defaultApiKey;
|
|
|
|
try {
|
|
packageStatus.value = PackageSyncStatus.downloading;
|
|
syncProgress.value = 0.1;
|
|
syncStatusMessage.value = 'جاري الاتصال بخادم الخرائط السيادي...';
|
|
|
|
// 1. Fetch Package Manifest
|
|
final manifestUrl = Uri.parse('$activeUrl/api/tactical/offline-package/jordan');
|
|
try {
|
|
final mResp = await http.get(manifestUrl, headers: {'x-api-key': activeKey}).timeout(const Duration(seconds: 6));
|
|
if (mResp.statusCode == 200) {
|
|
syncProgress.value = 0.35;
|
|
syncStatusMessage.value = 'تم استقبال بيانات الحزمة... جاري تحميل معالم PostGIS';
|
|
}
|
|
} catch (_) {
|
|
// Fallback to local cache
|
|
}
|
|
|
|
syncProgress.value = 0.50;
|
|
syncStatusMessage.value = 'جاري مزامنة معالم الأردن وتحديث جداول PostGIS...';
|
|
|
|
// 2. Fetch and Cache PostGIS Landmarks
|
|
final count = await LandmarkDatabase.syncFromServer(serverUrl: activeUrl, apiKey: activeKey);
|
|
debugPrint('Synced $count landmarks to local storage.');
|
|
|
|
syncProgress.value = 0.75;
|
|
syncStatusMessage.value = 'جاري تنزيل شبكة الطرق الحقيقية للأردن (غراف التوجيه + الارتفاعات)...';
|
|
|
|
// 3. Download & install the real Jordan road graph routing package
|
|
// (Valhalla tiles built from the same OSM data as server GraphHopper)
|
|
final routingOk = await OfflineRoutingPackageService.downloadAndInstall(
|
|
serverUrl: activeUrl,
|
|
apiKey: activeKey,
|
|
);
|
|
if (!routingOk) {
|
|
final hasLocal = await OfflineRoutingPackageService.isInstalled();
|
|
if (!hasLocal) {
|
|
debugPrint('Routing package failed to download and no local offline cache exists.');
|
|
syncStatusMessage.value = 'خطأ: تعذر تنزيل شبكة الطرق الأوفلاين. يرجى التحقق من الاتصال بالخادم.';
|
|
packageStatus.value = PackageSyncStatus.error;
|
|
return false;
|
|
} else {
|
|
debugPrint('Using previously installed local routing package.');
|
|
}
|
|
}
|
|
|
|
syncProgress.value = 0.90;
|
|
syncStatusMessage.value = 'جاري بناء وتحديث محرك التوجيه المحلي...';
|
|
|
|
// 4. Initialize & warm up on-device routing engine
|
|
OfflineRoutingEngine.initialize();
|
|
|
|
syncProgress.value = 0.95;
|
|
syncStatusMessage.value = 'جاري تثبيت الحزمة في الذاكرة الدائمة وتفعيل قفل الـ 14 يوماً...';
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool('${_pkgKey}_synced', true);
|
|
await prefs.setString('${_pkgKey}_date', DateTime.now().toIso8601String());
|
|
|
|
syncProgress.value = 1.0;
|
|
syncStatusMessage.value = 'اكتملت المزامنة بنجاح! الحزمة جاهزة للعمل 100% بدون إنترنت (تحديث قادم بعد 14 يوماً).';
|
|
packageStatus.value = PackageSyncStatus.installed;
|
|
|
|
return true;
|
|
} catch (e) {
|
|
debugPrint('Package download error: $e');
|
|
packageStatus.value = PackageSyncStatus.error;
|
|
syncStatusMessage.value = 'تعذر الاتصال بالسيرفر. تم تفعيل الحزمة المدمجة المسبقة.';
|
|
return false;
|
|
}
|
|
}
|
|
}
|