397 lines
14 KiB
Dart
397 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../config/app_config.dart';
|
|
import '../models/landmark.dart';
|
|
|
|
class LandmarkDatabase {
|
|
static const String _storageKey = 'cached_tactical_landmarks_v1';
|
|
static List<TacticalLandmark> _dynamicLandmarks = [];
|
|
static bool _isLoaded = false;
|
|
|
|
static final List<TacticalLandmark> _builtInLandmarks = const [
|
|
// ── Amman Region (العاصمة عمان) ───────────────────────────
|
|
TacticalLandmark(
|
|
id: 'amm-1',
|
|
name: 'مئذنة مسجد الملك حسين (دابوق)',
|
|
region: 'عمان الغربية',
|
|
type: LandmarkType.minaret,
|
|
lat: 31.9835,
|
|
lng: 35.8285,
|
|
elevationM: 1010,
|
|
description: 'مئذنة حجرية بارزة ترى بوضوح من غرب وشمال العاصمة وشارع الأردن',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-2',
|
|
name: 'برج اتصالات جبل التاج / جبل القلعة',
|
|
region: 'وسط عمان',
|
|
type: LandmarkType.tower,
|
|
lat: 31.9540,
|
|
lng: 35.9350,
|
|
elevationM: 850,
|
|
description: 'برج إرسال رئيسي مرتفع يشرف على وسط البلد وشرق عمان',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-3',
|
|
name: 'خزان مياه دابوق العالي (البرج الدائري)',
|
|
region: 'عمان الغربية',
|
|
type: LandmarkType.waterTank,
|
|
lat: 31.9960,
|
|
lng: 35.8150,
|
|
elevationM: 1045,
|
|
description: 'خزان خرساني ضخم على أعلى تلة في دابوق',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-4',
|
|
name: 'برج اتصالات صويلح (الدوريات الخارجية)',
|
|
region: 'شمال عمان',
|
|
type: LandmarkType.tower,
|
|
lat: 32.0250,
|
|
lng: 35.8450,
|
|
elevationM: 1070,
|
|
description: 'أعلى نقطة رصد شمال غرب العاصمة على تقاطع طريق الشمال',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-5',
|
|
name: 'مئذنة مسجد الملك المؤسس عبدالله الأول (العبدلي)',
|
|
region: 'وسط عمان',
|
|
type: LandmarkType.minaret,
|
|
lat: 31.9615,
|
|
lng: 35.9130,
|
|
elevationM: 880,
|
|
description: 'قبة زرقاء شهيرة ومئذنتان واضحتان في قلب العبدلي',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-6',
|
|
name: 'أبراج بوابة الأردن (أبراج الدوار السادس)',
|
|
region: 'عمان الغربية',
|
|
type: LandmarkType.tower,
|
|
lat: 31.9580,
|
|
lng: 35.8680,
|
|
elevationM: 990,
|
|
description: 'برجان توأم زجاجيان عاليان يشاهدان من جميع أطراف العاصمة',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-7',
|
|
name: 'صوامع حبوب الجيزة / المطار',
|
|
region: 'جنوب عمان',
|
|
type: LandmarkType.grainSilo,
|
|
lat: 31.6980,
|
|
lng: 35.9520,
|
|
elevationM: 730,
|
|
description: 'منشأة صوامع خرسانية عالية على الطريق الصحراوي الرئيسي',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'amm-8',
|
|
name: 'خزان مياه طبربور / عين غزال',
|
|
region: 'شمال شرق عمان',
|
|
type: LandmarkType.waterTank,
|
|
lat: 31.9890,
|
|
lng: 35.9420,
|
|
elevationM: 820,
|
|
description: 'برج مياه تكتيكي يشرف على اتوستراد عمان الزرقاء',
|
|
),
|
|
|
|
// ── Zarqa & Desert Corridor (الزرقاء والبادية) ─────────────
|
|
TacticalLandmark(
|
|
id: 'zrq-1',
|
|
name: 'مئذنة مسجد الشهيد راشد الزيود (غريسا)',
|
|
region: 'الزرقاء',
|
|
type: LandmarkType.minaret,
|
|
lat: 32.0910,
|
|
lng: 36.0950,
|
|
elevationM: 610,
|
|
description: 'مئذنة عالية تشرف على سهول الهاشمية وقرى الزرقاء',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'zrq-2',
|
|
name: 'قاعدة الشهيد موفق السلطي الجوية (الأزرق)',
|
|
region: 'الأزرق / الصحراء الشرقية',
|
|
type: LandmarkType.militaryBase,
|
|
lat: 31.8320,
|
|
lng: 36.7860,
|
|
elevationM: 520,
|
|
description: 'قاعدة جوية رئيسية ومعلم عسكري إستراتيجي في البادية الشرقية',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'zrq-3',
|
|
name: 'قلعة الأزرق التاريخية (قلعة البازلت)',
|
|
region: 'الأزرق',
|
|
type: LandmarkType.fort,
|
|
lat: 31.8800,
|
|
lng: 36.8270,
|
|
elevationM: 505,
|
|
description: 'حصن أثري من الحجر البازلتي الأسود يشرف على واحة الأزرق',
|
|
),
|
|
|
|
// ── North Region (إربد، عجلون، جرش، المفرق) ────────────────
|
|
TacticalLandmark(
|
|
id: 'irb-1',
|
|
name: 'برج اتصالات جامعة اليرموك (إربد)',
|
|
region: 'إربد',
|
|
type: LandmarkType.tower,
|
|
lat: 32.5340,
|
|
lng: 35.8540,
|
|
elevationM: 580,
|
|
description: 'برج إرسال رئيسي يشرف على كامل قاطع محافظة إربد وسهول حوران',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'irb-2',
|
|
name: 'مئذنة مسجد إربد الكبير',
|
|
region: 'وسط إربد',
|
|
type: LandmarkType.minaret,
|
|
lat: 32.5560,
|
|
lng: 35.8490,
|
|
elevationM: 590,
|
|
description: 'مئذنة تاريخية مميزة في مركز المدينة القديمة',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'ajl-1',
|
|
name: 'قلعة عجلون (قلعة الربض)',
|
|
region: 'عجلون',
|
|
type: LandmarkType.fort,
|
|
lat: 32.3250,
|
|
lng: 35.7270,
|
|
elevationM: 1020,
|
|
description: 'أعلى قمة عسكرية تاريخية تشرف على جبال الشمال وغور الأردن',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'jrs-1',
|
|
name: 'أعمدة معبد أرتميس (جرش الأثرية)',
|
|
region: 'جرش',
|
|
type: LandmarkType.fort,
|
|
lat: 32.2815,
|
|
lng: 35.8910,
|
|
elevationM: 600,
|
|
description: 'أعمدة أثرية ضخمة ترى بوضوح من تلال جرش المقابلة',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'mfq-1',
|
|
name: 'قاعدة الملك حسين الجوية (المفرق)',
|
|
region: 'المفرق',
|
|
type: LandmarkType.militaryBase,
|
|
lat: 32.3560,
|
|
lng: 36.2590,
|
|
elevationM: 680,
|
|
description: 'منشأة ومدرج طيران رئيسي في سهول الشمال الشرقي',
|
|
),
|
|
|
|
// ── Balqa & Salt (البلقاء والسلط والبحر الميت) ─────────────
|
|
TacticalLandmark(
|
|
id: 'slt-1',
|
|
name: 'مئذنة مسجد السلط الكبير',
|
|
region: 'السلط',
|
|
type: LandmarkType.minaret,
|
|
lat: 32.0390,
|
|
lng: 35.7280,
|
|
elevationM: 790,
|
|
description: 'مئذنة في وسط السلط تشرف على سفوح جبال البلقاء',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'slt-2',
|
|
name: 'قمة جبل نيبو (النبي موسى - مادبا)',
|
|
region: 'مادبا / البلقاء',
|
|
type: LandmarkType.mountain,
|
|
lat: 31.7680,
|
|
lng: 35.7250,
|
|
elevationM: 810,
|
|
description: 'مطل استراتيجي مرتفع يطل على وادي الأردن والبحر الميت والقدس',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'dms-1',
|
|
name: 'نقطة المراقبة البحرية (شمال البحر الميت)',
|
|
region: 'البحر الميت',
|
|
type: LandmarkType.militaryBase,
|
|
lat: 31.7200,
|
|
lng: 35.5800,
|
|
elevationM: -390,
|
|
description: 'برج مراقبة على الشاطئ الشمالي لأخفض نقطة على سطح الأرض',
|
|
),
|
|
|
|
// ── South Region (الكرك، الطفيلة، معان، العقبة) ───────────
|
|
TacticalLandmark(
|
|
id: 'krk-1',
|
|
name: 'قلعة الكرك التاريخية (برج الحصن)',
|
|
region: 'الكرك',
|
|
type: LandmarkType.fort,
|
|
lat: 31.1810,
|
|
lng: 35.7020,
|
|
elevationM: 950,
|
|
description: 'حصن صخري منيع يشرف على كامل قاطع الكرك والوديان الغربية',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'taf-1',
|
|
name: 'برج اتصالات جبل العيص (الطفيلة)',
|
|
region: 'الطفيلة',
|
|
type: LandmarkType.tower,
|
|
lat: 30.8420,
|
|
lng: 35.6180,
|
|
elevationM: 1250,
|
|
description: 'أعلى قمة في قاطع الطفيلة عليها أبراج رادار واتصالات',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'man-1',
|
|
name: 'قلعة الشوبك (مونتريال)',
|
|
region: 'معان / الشوبك',
|
|
type: LandmarkType.fort,
|
|
lat: 30.5315,
|
|
lng: 35.5610,
|
|
elevationM: 1330,
|
|
description: 'قلعة مخروطية عالية فوق تلة منعزلة ترى من مسافات بعيدة',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'aqb-1',
|
|
name: 'سارية علم الثورة العربية الكبرى (ساحل العقبة)',
|
|
region: 'العقبة',
|
|
type: LandmarkType.tower,
|
|
lat: 29.5215,
|
|
lng: 35.0015,
|
|
elevationM: 135,
|
|
description: 'سارية علم بطول 130 متراً تشاهد من كامل خليج العقبة والحدود',
|
|
),
|
|
TacticalLandmark(
|
|
id: 'aqb-2',
|
|
name: 'صوامع ميناء العقبة الجنوبي',
|
|
region: 'العقبة',
|
|
type: LandmarkType.grainSilo,
|
|
lat: 29.4120,
|
|
lng: 34.9810,
|
|
elevationM: 25,
|
|
description: 'صوامع ومنشآت تخزين عملاقة على الساحل الجنوبي',
|
|
),
|
|
];
|
|
|
|
static List<TacticalLandmark> get allLandmarks {
|
|
if (_dynamicLandmarks.isEmpty) {
|
|
return _builtInLandmarks;
|
|
}
|
|
// Merge dynamic with built-in by unique ID
|
|
final map = <String, TacticalLandmark>{};
|
|
for (final lm in _builtInLandmarks) {
|
|
map[lm.id] = lm;
|
|
}
|
|
for (final lm in _dynamicLandmarks) {
|
|
map[lm.id] = lm;
|
|
}
|
|
return map.values.toList();
|
|
}
|
|
|
|
/// Initialize local cache from persistent storage (permanent documents directory)
|
|
static Future<void> initCache() async {
|
|
if (_isLoaded) return;
|
|
try {
|
|
// 1. Try reading from permanent application documents file
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final dbFile = File('${dir.path}/jordan_tactical_landmarks.json');
|
|
if (await dbFile.exists()) {
|
|
final rawJson = await dbFile.readAsString();
|
|
if (rawJson.isNotEmpty) {
|
|
final List<dynamic> list = jsonDecode(rawJson);
|
|
_dynamicLandmarks = list.map((item) => _parseLandmark(item)).toList();
|
|
_isLoaded = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. Fallback to SharedPreferences
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final rawJson = prefs.getString(_storageKey);
|
|
if (rawJson != null && rawJson.isNotEmpty) {
|
|
final List<dynamic> list = jsonDecode(rawJson);
|
|
_dynamicLandmarks = list.map((item) => _parseLandmark(item)).toList();
|
|
}
|
|
_isLoaded = true;
|
|
} catch (e) {
|
|
debugPrint('Landmark cache init error: $e');
|
|
}
|
|
}
|
|
|
|
/// Sync landmarks from Server API and update local permanent storage
|
|
static Future<int> syncFromServer({
|
|
String? serverUrl,
|
|
String? apiKey,
|
|
}) async {
|
|
final activeUrl = serverUrl ?? AppConfig.serverUrl;
|
|
final activeKey = apiKey ?? AppConfig.apiKey;
|
|
|
|
try {
|
|
final url = Uri.parse('$activeUrl/api/tactical/landmarks');
|
|
final resp = await http.get(url, headers: {'x-api-key': activeKey}).timeout(const Duration(seconds: 10));
|
|
|
|
if (resp.statusCode == 200) {
|
|
final data = jsonDecode(resp.body);
|
|
if (data['landmarks'] != null && data['landmarks'] is List) {
|
|
final List<dynamic> rawList = data['landmarks'];
|
|
_dynamicLandmarks = rawList.map((item) => _parseLandmark(item)).toList();
|
|
|
|
// Save to permanent documents directory
|
|
try {
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final dbFile = File('${dir.path}/jordan_tactical_landmarks.json');
|
|
await dbFile.writeAsString(jsonEncode(rawList));
|
|
} catch (fileErr) {
|
|
debugPrint('Error writing landmarks file: $fileErr');
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_storageKey, jsonEncode(rawList));
|
|
return _dynamicLandmarks.length;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Sync landmarks error: $e');
|
|
}
|
|
return _dynamicLandmarks.length;
|
|
}
|
|
|
|
static TacticalLandmark _parseLandmark(dynamic json) {
|
|
LandmarkType parseType(String? t) {
|
|
switch (t) {
|
|
case 'minaret': return LandmarkType.minaret;
|
|
case 'tower': return LandmarkType.tower;
|
|
case 'waterTank': return LandmarkType.waterTank;
|
|
case 'fort': return LandmarkType.fort;
|
|
case 'mountain': return LandmarkType.mountain;
|
|
case 'militaryBase': return LandmarkType.militaryBase;
|
|
case 'grainSilo': return LandmarkType.grainSilo;
|
|
default: return LandmarkType.tower;
|
|
}
|
|
}
|
|
|
|
return TacticalLandmark(
|
|
id: json['id'] ?? 'lm-${DateTime.now().millisecondsSinceEpoch}',
|
|
name: json['name'] ?? 'معلم تكتيكي',
|
|
region: json['region'] ?? 'الأردن',
|
|
type: parseType(json['type']),
|
|
lat: (json['lat'] as num).toDouble(),
|
|
lng: (json['lng'] as num).toDouble(),
|
|
elevationM: (json['elevationM'] as num?)?.toInt() ?? 800,
|
|
description: json['description'] ?? '',
|
|
);
|
|
}
|
|
|
|
static List<TacticalLandmark> search(String query, {String? region, LandmarkType? type}) {
|
|
final cleanQuery = query.trim().toLowerCase();
|
|
|
|
return allLandmarks.where((landmark) {
|
|
if (region != null && region.isNotEmpty && region != 'الكل') {
|
|
if (!landmark.region.contains(region)) return false;
|
|
}
|
|
|
|
if (type != null) {
|
|
if (landmark.type != type) return false;
|
|
}
|
|
|
|
if (cleanQuery.isEmpty) return true;
|
|
|
|
return landmark.name.toLowerCase().contains(cleanQuery) ||
|
|
landmark.region.toLowerCase().contains(cleanQuery) ||
|
|
landmark.description.toLowerCase().contains(cleanQuery) ||
|
|
landmark.typeLabelArabic.contains(cleanQuery);
|
|
}).toList();
|
|
}
|
|
}
|