Files
maps-saas/apps/flutter_map_demo/lib/main.dart
T

557 lines
18 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart' show kIsWeb;
void main() {
runApp(
const MaterialApp(home: MapScreen(), debugShowCheckedModeBanner: false),
);
}
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
MapLibreMapController? mapController;
bool isLoading = false;
String info = '📍 نظام انطلق المتكامل';
bool styleLoaded = false;
bool isSyncing = false;
bool isContinuousSyncEnabled = true; // Default to ON for premium experience
double syncProgress = 0.0;
// --- Feature: Custom Car Marker ---
Future<void> _addCarMarker() async {
if (!styleLoaded || mapController == null) return;
try {
// Load the car icon image (from network for quick demo)
final ByteData bytes = await NetworkAssetBundle(
Uri.parse(
'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Car_icon_blue.png/64px-Car_icon_blue.png',
),
).load('');
final Uint8List list = bytes.buffer.asUint8List();
await mapController!.addImage("car-icon", list);
await mapController!.addSymbol(
SymbolOptions(
geometry: const LatLng(33.513, 36.276), // Damascus city center
iconImage: "car-icon",
iconSize: 0.5,
textField: "سيارة انطلاقة #101",
textOffset: const Offset(0, 2),
textColor: "#3b82f6",
textHaloColor: "#ffffff",
textHaloWidth: 2,
),
);
setState(() => info = '✅ تمت إضافة علامة السيارة');
} catch (e) {
setState(() => info = 'خطأ في إضافة العلامة: $e');
}
}
// --- Feature: Polylines (Lines) ---
Future<void> _addSampleLine() async {
if (!styleLoaded || mapController == null) return;
await mapController!.addLine(
LineOptions(
geometry: [
const LatLng(33.515, 36.270),
const LatLng(33.520, 36.280),
const LatLng(33.510, 36.290),
],
lineColor: "#f59e0b",
lineWidth: 5.0,
lineOpacity: 0.8,
),
);
setState(() => info = '✅ تمت إضافة خط مسار');
}
// --- Feature: Polygons (Geofence) ---
Future<void> _addSamplePolygon() async {
if (!styleLoaded || mapController == null) return;
await mapController!.addFill(
FillOptions(
geometry: [
[
const LatLng(33.518, 36.273),
const LatLng(33.522, 36.273),
const LatLng(33.522, 36.3),
const LatLng(33.518, 36.4),
const LatLng(33.518, 36.273),
],
],
fillColor: "#10b981",
fillOpacity: 0.3,
fillOutlineColor: "#065f46",
),
);
setState(() => info = '✅ تمت إضافة سياج جغرافي');
}
// --- Feature: Circles ---
Future<void> _addSampleCircle() async {
if (!styleLoaded || mapController == null) return;
await mapController!.addCircle(
CircleOptions(
geometry: const LatLng(33.512, 36.280),
circleColor: "#ef4444",
circleRadius: 15.0,
circleOpacity: 0.6,
circleStrokeColor: "#ffffff",
circleStrokeWidth: 2.0,
),
);
setState(() => info = '✅ تمت إضافة نقطة محيطية');
}
// --- Feature: Routing ---
Future<void> computeRoute() async {
if (!styleLoaded || mapController == null) return;
setState(() {
isLoading = true;
info = '⚡ جاري حساب المسار...';
});
try {
final res = await http.get(
Uri.parse(
'https://map-saas.intaleqapp.com/api/maps/route?fromLat=33.513&fromLng=36.276&toLat=33.530&toLng=36.310',
),
headers: {'x-api-key': 'intaleq_secret_2026'},
);
final data = json.decode(res.body);
final pts = _decodePoly(data['points']);
await mapController?.clearLines();
await mapController?.addLine(
LineOptions(geometry: pts, lineColor: "#3b82f6", lineWidth: 6.0),
);
final latList = pts.map((p) => p.latitude).toList()..sort();
final lngList = pts.map((p) => p.longitude).toList()..sort();
mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(latList.first, lngList.first),
northeast: LatLng(latList.last, lngList.last),
),
left: 50,
right: 50,
top: 100,
bottom: 100,
),
);
setState(
() => info =
'🏁 ${(data['distance'] / 1000).toStringAsFixed(1)} كم | ${(data['duration'] / 60).toStringAsFixed(0)} دقيقة',
);
} catch (e) {
setState(() => info = 'خطأ: $e');
} finally {
setState(() => isLoading = false);
}
}
// --- Feature: Fetch and Display Saved Places ---
Future<void> _fetchUserPlaces() async {
if (!styleLoaded || mapController == null) return;
try {
final res = await http.get(
Uri.parse('https://map-saas.intaleqapp.com/api/geocoding/places'),
headers: {'x-api-key': 'intaleq_secret_2026'},
);
if (res.statusCode == 200) {
final List<dynamic> data = json.decode(res.body);
await mapController?.clearSymbols();
for (var place in data) {
final lat = double.tryParse(place['latitude'].toString());
final lng = double.tryParse(place['longitude'].toString());
if (lat != null && lng != null) {
await mapController?.addSymbol(
SymbolOptions(
geometry: LatLng(lat, lng),
iconImage:
"marker-15", // Default MapLibre icon if using standard style
iconSize: 1.5,
textField: place['name_ar'] ?? place['name'],
textOffset: const Offset(0, 1.5),
textColor: "#1e293b",
textHaloColor: "#ffffff",
textHaloWidth: 2,
),
);
}
}
setState(() => info = '📍 تم تحميل ${data.length} موقع من النظام');
}
} catch (e) {
debugPrint('Error fetching places: $e');
}
}
// --- Feature: Offline Map Caching (Mobile Only) ---
Future<void> _downloadCurrentRegion() async {
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'التحميل اليدوي غير متاح على المتصفح (المتصفح يستخدم التخزين المؤقت للمتصفح تلقائياً)',
),
),
);
return;
}
if (mapController == null) return;
final region = await mapController!.getVisibleRegion();
final zoom = mapController!.cameraPosition?.zoom ?? 14.0;
setState(() {
isLoading = true;
info = '⬇️ جاري بدء التحميل...';
});
try {
// Create an offline region for current view
// Zoom levels from current zoom -1 to +2
await downloadOfflineRegion(
OfflineRegionDefinition(
bounds: region,
// تم التعديل هنا: محرك iOS يتطلب رابط إنترنت (URL) حقيقي لعملية الـ Offline
mapStyleUrl:
"https://map-saas.intaleqapp.com/styles/style.json", // يرجى التأكد من رفع ملف الستايل الخاص بك على هذا الرابط
minZoom: zoom.floor() - 1.0,
maxZoom: zoom.floor() + 2.0,
),
metadata: {
'name': 'Manual Download ${DateTime.now().toIso8601String()}',
},
);
setState(() => info = '✅ بدأ تحميل الخرائط لجهازك (Mobile Only)');
} catch (e) {
setState(() => info = '❌ خطأ في التحميل: $e');
} finally {
setState(() => isLoading = false);
}
}
Future<void> _clearMapCache() async {
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'الرجاء مسح بيانات المتصفح (Browser Cache) لإفراغ الذاكرة',
),
),
);
return;
}
// On mobile, we can list and delete regions
// For now, simple info update
setState(() => info = '🗑️ تم إرسال طلب مسح الذاكرة المؤقتة');
}
// --- Feature: Automatic Damascus Sync (Mobile Only) ---
Future<void> _initAutoOfflineSync() async {
if (kIsWeb) return;
try {
// Damascus City Center Bounds roughly +/- 0.1 deg
final damascusBounds = LatLngBounds(
southwest: const LatLng(33.473, 36.226),
northeast: const LatLng(33.583, 36.356),
);
setState(() {
isSyncing = true;
info = '🔄 جاري تأمين خريطة دمشق (Offline)...';
});
await downloadOfflineRegion(
OfflineRegionDefinition(
bounds: damascusBounds,
mapStyleUrl: "https://map-saas.intaleqapp.com/styles/style.json",
minZoom: 0.0,
maxZoom: 16.0, // High detail for Damascus
),
metadata: {'name': 'Damascus_Auto_Sync'},
// onHttpError: (error) => debugPrint('Sync HTTP Error: $error'),
// onTileError: (error) => debugPrint('Sync Tile Error: $error'),
);
setState(() {
isSyncing = false;
info = '✅ تم تأمين دمشق بالكامل للعمل أوفلاين';
});
} catch (e) {
debugPrint('Sync failed: $e');
setState(() => isSyncing = false);
}
}
// --- Feature: Continuous Background Sync (on move) ---
Future<void> _handleCameraIdleSync() async {
if (kIsWeb || !isContinuousSyncEnabled || mapController == null) return;
final bounds = await mapController!.getVisibleRegion();
final zoom = mapController!.cameraPosition?.zoom ?? 14.0;
debugPrint('Continuous Sync: Triggering for new view at zoom $zoom');
// Silent download for the current region
// We don't update 'info' here to keep it subtle, or just use a small indicator
try {
await downloadOfflineRegion(
OfflineRegionDefinition(
bounds: bounds,
mapStyleUrl: "https://map-saas.intaleqapp.com/styles/style.json",
minZoom: zoom.floor() - 1.0,
maxZoom: zoom.floor() + 2.0,
),
metadata: {'name': 'Auto_Area_${DateTime.now().millisecondsSinceEpoch}'},
);
} catch (e) {
debugPrint('Silent sync error (normal if redundant): $e');
}
}
List<LatLng> _decodePoly(dynamic p) {
if (p is String) {
var l = <LatLng>[];
int index = 0, lat = 0, lng = 0;
while (index < p.length) {
int b, shift = 0, res = 0;
do {
b = p.codeUnitAt(index++) - 63;
res |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
lat += (res & 1) != 0 ? ~(res >> 1) : (res >> 1);
shift = 0;
res = 0;
do {
b = p.codeUnitAt(index++) - 63;
res |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
lng += (res & 1) != 0 ? ~(res >> 1) : (res >> 1);
l.add(LatLng(lat / 1e5, lng / 1e5));
}
return l;
} else if (p is List) {
return (p as List)
.map<LatLng>((e) => LatLng(e[1].toDouble(), e[0].toDouble()))
.toList();
}
return [];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Intaleq Advanced Maps',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
centerTitle: true,
backgroundColor: const Color(0xFF0f172a),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Color(0xFF0f172a)),
child: Center(
child: Text(
'أدوات انطلاقة المتقدمة',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
ListTile(
leading: const Icon(Icons.location_on, color: Colors.blue),
title: const Text('إضافة علامة سيارة'),
onTap: () {
Navigator.pop(context);
_addCarMarker();
},
),
ListTile(
leading: const Icon(Icons.timeline, color: Colors.orange),
title: const Text('رسم خط مسار (Line)'),
onTap: () {
Navigator.pop(context);
_addSampleLine();
},
),
ListTile(
leading: const Icon(Icons.layers, color: Colors.green),
title: const Text('رسم سياج جغرافي (Polygon)'),
onTap: () {
Navigator.pop(context);
_addSamplePolygon();
},
),
ListTile(
leading: const Icon(
Icons.radio_button_checked,
color: Colors.red,
),
title: const Text('رسم دائرة (Circle)'),
onTap: () {
Navigator.pop(context);
_addSampleCircle();
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.refresh, color: Colors.blue),
title: const Text('تحديث المواقع المسجلة'),
onTap: () {
Navigator.pop(context);
_fetchUserPlaces();
},
),
ListTile(
leading: const Icon(Icons.directions, color: Colors.blueAccent),
title: const Text('اختبار المسار التلقائي'),
onTap: () {
Navigator.pop(context);
computeRoute();
},
),
const Divider(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'إدارة الخرائط (Offline)',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
),
ListTile(
leading: Icon(
Icons.download_for_offline,
color: kIsWeb ? Colors.grey : Colors.green,
),
title: const Text('تحميل النطاق الحالي للجهاز'),
onTap: () {
Navigator.pop(context);
_downloadCurrentRegion();
},
),
SwitchListTile(
secondary: Icon(Icons.sync, color: kIsWeb ? Colors.grey : Colors.blue),
title: const Text('المزامنة المستمرة (Dynamic Sync)'),
subtitle: const Text('حفظ أي منطقة يتم استكشافها تلقائياً', style: TextStyle(fontSize: 10)),
value: isContinuousSyncEnabled,
onChanged: kIsWeb ? null : (val) => setState(() => isContinuousSyncEnabled = val),
),
ListTile(
leading: Icon(
Icons.delete_sweep,
color: kIsWeb ? Colors.grey : Colors.redAccent,
),
title: const Text('مسح الذاكرة المؤقتة'),
onTap: () {
Navigator.pop(context);
_clearMapCache();
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.delete_forever, color: Colors.grey),
title: const Text('مسح كافة البيانات (UI)'),
onTap: () {
Navigator.pop(context);
mapController?.clearLines();
mapController?.clearSymbols();
mapController?.clearFills();
mapController?.clearCircles();
},
),
],
),
),
body: Stack(
children: [
MapLibreMap(
styleString: "assets/style.json",
initialCameraPosition: const CameraPosition(
target: LatLng(33.513, 36.276),
zoom: 14,
),
onMapCreated: (MapLibreMapController c) => mapController = c,
onStyleLoadedCallback: () async {
setState(() => styleLoaded = true);
// Load Premium Icons into the Map Controller
final icons = ['hospital', 'police', 'pharmacy', 'restaurant', 'cafe', 'shop', 'tourist', 'train', 'arrow'];
for (final icon in icons) {
try {
final ByteData bytes = await rootBundle.load('assets/icons/$icon.svg');
await mapController?.addImage(icon, bytes.buffer.asUint8List());
} catch (e) {
debugPrint('Error loading asset icon $icon: $e');
}
}
_fetchUserPlaces();
_initAutoOfflineSync();
},
onCameraIdle: _handleCameraIdleSync, // Trigger continuous sync when movement stops
),
if (!styleLoaded)
Container(
color: const Color(0xFFf8f9fa),
child: const Center(child: CircularProgressIndicator()),
),
Positioned(
top: 10,
left: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(30),
),
child: Text(
info,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
if (isLoading) const Center(child: CircularProgressIndicator()),
],
),
);
}
}