25-10-5/1
This commit is contained in:
62
lib/controller/home/decode_polyline_isolate.dart
Normal file
62
lib/controller/home/decode_polyline_isolate.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
List<LatLng> decodePolylineIsolate(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
int b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
// Helper method for decoding polyline (if not already defined)
|
||||
// List<LatLng> decodePolyline(String encoded) {
|
||||
// List<LatLng> points = [];
|
||||
// int index = 0, len = encoded.length;
|
||||
// int lat = 0, lng = 0;
|
||||
|
||||
// while (index < len) {
|
||||
// int b, shift = 0, result = 0;
|
||||
// do {
|
||||
// b = encoded.codeUnitAt(index++) - 63;
|
||||
// result |= (b & 0x1f) << shift;
|
||||
// shift += 5;
|
||||
// } while (b >= 0x20);
|
||||
// int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
// lat += dlat;
|
||||
|
||||
// shift = 0;
|
||||
// result = 0;
|
||||
// do {
|
||||
// b = encoded.codeUnitAt(index++) - 63;
|
||||
// result |= (b & 0x1f) << shift;
|
||||
// shift += 5;
|
||||
// } while (b >= 0x20);
|
||||
// int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
// lng += dlng;
|
||||
|
||||
// points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
// }
|
||||
|
||||
// return points;
|
||||
// }
|
||||
@@ -6,6 +6,7 @@ import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import 'dart:convert';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:Intaleq/constant/univeries_polygon.dart';
|
||||
@@ -22,7 +23,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||
// import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:location/location.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
@@ -52,6 +53,7 @@ import '../functions/launch.dart';
|
||||
import '../functions/package_info.dart';
|
||||
import '../functions/secure_storage.dart';
|
||||
import '../payment/payment_controller.dart';
|
||||
import 'decode_polyline_isolate.dart';
|
||||
import 'deep_link_controller.dart';
|
||||
import 'device_tier.dart';
|
||||
import 'vip_waitting_page.dart';
|
||||
@@ -1035,7 +1037,7 @@ class MapPassengerController extends GetxController {
|
||||
if (isBeginRideFromDriverRunning) return; // Prevent duplicate streams
|
||||
isBeginRideFromDriverRunning = true;
|
||||
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getRideStatusBegin, payload: {'ride_id': rideId});
|
||||
@@ -1138,13 +1140,14 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
Map<String, dynamic> tripData =
|
||||
box.read(BoxName.tripData) as Map<String, dynamic>;
|
||||
final points = decodePolyline(
|
||||
tripData["routes"][0]["overview_polyline"]["points"]);
|
||||
String pointsString =
|
||||
(tripData["routes"][0]["overview_polyline"]["points"]);
|
||||
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinates.add(LatLng(lat, lng));
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < decodedPoints.length; i++) {
|
||||
polylineCoordinates.add(decodedPoints[i]);
|
||||
}
|
||||
var polyline = Polyline(
|
||||
polylineId: const PolylineId('begin trip'),
|
||||
@@ -1819,6 +1822,7 @@ class MapPassengerController extends GetxController {
|
||||
await getCarsLocationByPassengerAndReloadMarker(
|
||||
box.read(BoxName.carType), reloadDuration);
|
||||
// await getNearestDriverByPassengerLocation();
|
||||
currentTimeSearchingCaptainWindow = 0;
|
||||
driversStatusForSearchWindow = 'We are search for nearst driver'.tr;
|
||||
if (isDriversDataValid()) {
|
||||
driversFound = true;
|
||||
@@ -2142,8 +2146,9 @@ class MapPassengerController extends GetxController {
|
||||
} else if (rideStatusDelayed == 'Apply' ||
|
||||
rideStatusDelayed == 'Applied') {
|
||||
isApplied = true;
|
||||
rideAppliedFromDriver(isApplied);
|
||||
timer.cancel();
|
||||
rideAppliedFromDriver(isApplied);
|
||||
// timer.cancel();
|
||||
// Close stream after applying
|
||||
} else if (attemptCounter >= maxAttempts ||
|
||||
rideStatusDelayed == 'waiting') {
|
||||
@@ -2186,7 +2191,7 @@ class MapPassengerController extends GetxController {
|
||||
rideAppliedFromDriver(bool isApplied) async {
|
||||
await getUpdatedRideForDriverApply(rideId);
|
||||
NotificationController().showNotification(
|
||||
'Accepted Ride'.tr,
|
||||
'Accepted Ride',
|
||||
'$driverName ${'accepted your order at price'.tr} ${totalPassenger.toStringAsFixed(1)} ${'with type'.tr} ${box.read(BoxName.carType)}',
|
||||
'ding');
|
||||
if (box.read(BoxName.carType) == 'Speed' ||
|
||||
@@ -2306,7 +2311,7 @@ class MapPassengerController extends GetxController {
|
||||
// driversToken.remove(driverToken);
|
||||
// for (var i = 1; i < driversToken.length; i++) {
|
||||
firebaseMessagesController.sendNotificationToDriverMAP(
|
||||
'Order Accepted'.tr,
|
||||
'Order Accepted',
|
||||
'$driverName${'Accepted your order'.tr}',
|
||||
driverToken.toString(),
|
||||
[],
|
||||
@@ -3514,57 +3519,71 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371.0; // km
|
||||
final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||
final dLon = (lon2 - lon1) * math.pi / 180.0;
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(lat1 * math.pi / 180.0) *
|
||||
math.cos(lat2 * math.pi / 180.0) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
// double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
// const R = 6371.0; // km
|
||||
// final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||
// final dLon = (lon2 - lon1) * math.pi / 180.0;
|
||||
// final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
// math.cos(lat1 * math.pi / 180.0) *
|
||||
// math.cos(lat2 * math.pi / 180.0) *
|
||||
// math.sin(dLon / 2) *
|
||||
// math.sin(dLon / 2);
|
||||
// final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
// return R * c;
|
||||
// }
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
double _kmToLatDelta(double km) => km / 111.0;
|
||||
// double _kmToLatDelta(double km) => km / 111.0;
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
|
||||
double _kmToLngDelta(double km, double atLat) =>
|
||||
km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
|
||||
// /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
|
||||
// double _kmToLngDelta(double km, double atLat) =>
|
||||
// km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
|
||||
|
||||
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
|
||||
double _relevanceScore(String name, String query) {
|
||||
final n = name.toLowerCase();
|
||||
final parts =
|
||||
query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
|
||||
double s = 0.0;
|
||||
for (final p in parts) {
|
||||
if (n.startsWith(p)) {
|
||||
s += 2.0;
|
||||
} else if (n.contains(p)) {
|
||||
s += 1.0;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// double _relevanceScore(String name, String query) {
|
||||
// final n = name.toLowerCase();
|
||||
// final parts =
|
||||
// query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
|
||||
// double s = 0.0;
|
||||
// for (final p in parts) {
|
||||
// if (n.startsWith(p)) {
|
||||
// s += 2.0;
|
||||
// } else if (n.contains(p)) {
|
||||
// s += 1.0;
|
||||
// }
|
||||
// }
|
||||
// return s;
|
||||
// }
|
||||
// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// --== الدالة الرئيسية للبحث ==--
|
||||
// -----------------------------------------------------------------
|
||||
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// --== الدالة الرئيسية للبحث ==--
|
||||
// -----------------------------------------------------------------
|
||||
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
Future<void> getPlaces() async {
|
||||
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
|
||||
final q = placeDestinationController.text.trim();
|
||||
if (q.isEmpty) {
|
||||
if (q.isEmpty || q.length < 3) {
|
||||
// يفضل عدم البحث قبل 3 أحرف
|
||||
placesDestination = [];
|
||||
update();
|
||||
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
|
||||
return;
|
||||
}
|
||||
|
||||
final lat = passengerLocation.latitude;
|
||||
final lng = passengerLocation.longitude;
|
||||
|
||||
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
|
||||
// نصف قطر البحث بالكيلومتر
|
||||
const radiusKm = 200.0;
|
||||
|
||||
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة)
|
||||
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
|
||||
final latDelta = _kmToLatDelta(radiusKm);
|
||||
final lngDelta = _kmToLngDelta(radiusKm, lat);
|
||||
|
||||
@@ -3574,6 +3593,7 @@ class MapPassengerController extends GetxController {
|
||||
final lngMax = lng + lngDelta;
|
||||
|
||||
try {
|
||||
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getPlacesSyria,
|
||||
payload: {
|
||||
@@ -3585,59 +3605,106 @@ class MapPassengerController extends GetxController {
|
||||
},
|
||||
);
|
||||
|
||||
// يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...]
|
||||
// --- [تم الإصلاح هنا] ---
|
||||
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
|
||||
List list;
|
||||
if (response is Map && response['message'] is List) {
|
||||
list = List.from(response['message'] as List);
|
||||
if (response is Map) {
|
||||
if (response['status'] == 'success' && response['message'] is List) {
|
||||
list = List.from(response['message'] as List);
|
||||
} else if (response['status'] == 'failure') {
|
||||
print('Server Error: ${response['message']}');
|
||||
return;
|
||||
} else {
|
||||
print('Unexpected Map shape from server');
|
||||
return;
|
||||
}
|
||||
} else if (response is List) {
|
||||
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
|
||||
list = List.from(response);
|
||||
} else {
|
||||
print('Unexpected response shape');
|
||||
print('Unexpected response shape from server');
|
||||
return;
|
||||
}
|
||||
|
||||
// جهّز الحقول المحتملة للأسماء
|
||||
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
|
||||
|
||||
// دالة مساعدة لاختيار أفضل اسم متاح
|
||||
String _bestName(Map p) {
|
||||
return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString();
|
||||
return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
|
||||
}
|
||||
|
||||
// احسب المسافة ودرجة التطابق والنقاط
|
||||
// حساب المسافة والصلة والنقاط النهائية لكل نتيجة
|
||||
for (final p in list) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0;
|
||||
final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0;
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
|
||||
final d = _haversineKm(lat, lng, plat, plng);
|
||||
final rel = _relevanceScore(_bestName(p), q);
|
||||
final distance = _haversineKm(lat, lng, plat, plng);
|
||||
final relevance = _relevanceScore(_bestName(p), q);
|
||||
|
||||
// معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى
|
||||
// تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق
|
||||
final score = (1.0 / (1.0 + d)) * (1.0 + rel);
|
||||
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
|
||||
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
|
||||
|
||||
p['distanceKm'] = d;
|
||||
p['relevance'] = rel;
|
||||
p['distanceKm'] = distance;
|
||||
p['relevance'] = relevance;
|
||||
p['score'] = score;
|
||||
}
|
||||
|
||||
// رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم
|
||||
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
|
||||
list.sort((a, b) {
|
||||
final sa = (a['score'] ?? 0.0) as double;
|
||||
final sb = (b['score'] ?? 0.0) as double;
|
||||
final cmp = sb.compareTo(sa);
|
||||
if (cmp != 0) return cmp;
|
||||
final da = (a['distanceKm'] ?? 1e9) as double;
|
||||
final db = (b['distanceKm'] ?? 1e9) as double;
|
||||
return da.compareTo(db);
|
||||
return sb.compareTo(sa);
|
||||
});
|
||||
|
||||
// خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل
|
||||
placesDestination = list.take(15).toList();
|
||||
Log.print('placesDestination: $placesDestination');
|
||||
placesDestination = list;
|
||||
print('Updated places: $placesDestination');
|
||||
update();
|
||||
} catch (e) {
|
||||
print('Exception in getPlaces: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// --== دوال مساعدة ==--
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين)
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371.0; // نصف قطر الأرض بالكيلومتر
|
||||
final dLat = (lat2 - lat1) * (pi / 180.0);
|
||||
final dLon = (lon2 - lon1) * (pi / 180.0);
|
||||
final rLat1 = lat1 * (pi / 180.0);
|
||||
final rLat2 = lat2 * (pi / 180.0);
|
||||
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2);
|
||||
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث
|
||||
double _relevanceScore(String placeName, String query) {
|
||||
if (placeName.isEmpty || query.isEmpty) return 0.0;
|
||||
final pLower = placeName.toLowerCase();
|
||||
final qLower = query.toLowerCase();
|
||||
if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية
|
||||
if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// تحويل كيلومتر إلى فرق درجات لخط العرض
|
||||
double _kmToLatDelta(double km) {
|
||||
const kmInDegree = 111.32;
|
||||
return km / kmInDegree;
|
||||
}
|
||||
|
||||
/// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي)
|
||||
double _kmToLngDelta(double km, double latitude) {
|
||||
const kmInDegree = 111.32;
|
||||
return km / (kmInDegree * cos(latitude * (pi / 180.0)));
|
||||
}
|
||||
|
||||
// var languageCode;
|
||||
|
||||
// // تحديد اللغة حسب الإدخال
|
||||
@@ -3786,27 +3853,102 @@ class MapPassengerController extends GetxController {
|
||||
// update();
|
||||
// }
|
||||
|
||||
Future getPlacesStart() async {
|
||||
var languageCode = wayPoint0Controller.text;
|
||||
|
||||
// Regular expression to check for English alphabet characters
|
||||
final englishRegex = RegExp(r'[a-zA-Z]');
|
||||
|
||||
// Check if text contains English characters
|
||||
if (englishRegex.hasMatch(languageCode)) {
|
||||
languageCode = 'en';
|
||||
} else {
|
||||
languageCode = 'ar';
|
||||
Future<void> getPlacesStart() async {
|
||||
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
|
||||
final q = placeStartController.text.trim();
|
||||
if (q.isEmpty || q.length < 3) {
|
||||
// يفضل عدم البحث قبل 3 أحرف
|
||||
placesStart = [];
|
||||
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
|
||||
return;
|
||||
}
|
||||
|
||||
var url =
|
||||
// '${AppLink.googleMapsLink}place/nearbysearch/json?location=${mylocation.longitude}&radius=25000&language=ar&keyword=&key=${placeController.text}${AK.mapAPIKEY}';
|
||||
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=${placeStartController.text}&location=${passengerLocation.latitude},${passengerLocation.longitude}&radius=250000&language=$languageCode&key=${AK.mapAPIKEY.toString()}';
|
||||
final lat = passengerLocation.latitude;
|
||||
final lng = passengerLocation.longitude;
|
||||
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
// نصف قطر البحث بالكيلومتر
|
||||
const radiusKm = 200.0;
|
||||
|
||||
placesStart = response['results'];
|
||||
update();
|
||||
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
|
||||
final latDelta = _kmToLatDelta(radiusKm);
|
||||
final lngDelta = _kmToLngDelta(radiusKm, lat);
|
||||
|
||||
final latMin = lat - latDelta;
|
||||
final latMax = lat + latDelta;
|
||||
final lngMin = lng - lngDelta;
|
||||
final lngMax = lng + lngDelta;
|
||||
|
||||
try {
|
||||
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getPlacesSyria,
|
||||
payload: {
|
||||
'query': q,
|
||||
'lat_min': latMin.toString(),
|
||||
'lat_max': latMax.toString(),
|
||||
'lng_min': lngMin.toString(),
|
||||
'lng_max': lngMax.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
// --- [تم الإصلاح هنا] ---
|
||||
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
|
||||
List list;
|
||||
if (response is Map) {
|
||||
if (response['status'] == 'success' && response['message'] is List) {
|
||||
list = List.from(response['message'] as List);
|
||||
} else if (response['status'] == 'failure') {
|
||||
print('Server Error: ${response['message']}');
|
||||
return;
|
||||
} else {
|
||||
print('Unexpected Map shape from server');
|
||||
return;
|
||||
}
|
||||
} else if (response is List) {
|
||||
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
|
||||
list = List.from(response);
|
||||
} else {
|
||||
print('Unexpected response shape from server');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
|
||||
|
||||
// دالة مساعدة لاختيار أفضل اسم متاح
|
||||
String _bestName(Map p) {
|
||||
return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
|
||||
}
|
||||
|
||||
// حساب المسافة والصلة والنقاط النهائية لكل نتيجة
|
||||
for (final p in list) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
|
||||
final distance = _haversineKm(lat, lng, plat, plng);
|
||||
final relevance = _relevanceScore(_bestName(p), q);
|
||||
|
||||
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
|
||||
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
|
||||
|
||||
p['distanceKm'] = distance;
|
||||
p['relevance'] = relevance;
|
||||
p['score'] = score;
|
||||
}
|
||||
|
||||
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
|
||||
list.sort((a, b) {
|
||||
final sa = (a['score'] ?? 0.0) as double;
|
||||
final sb = (b['score'] ?? 0.0) as double;
|
||||
return sb.compareTo(sa);
|
||||
});
|
||||
|
||||
placesStart = list;
|
||||
print('Updated places: $placesDestination');
|
||||
update();
|
||||
} catch (e) {
|
||||
print('Exception in getPlaces: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future getPlacesListsWayPoint(int index) async {
|
||||
@@ -4557,12 +4699,15 @@ class MapPassengerController extends GetxController {
|
||||
data[0]["start_location"]['lat'], data[0]["start_location"]['lng']);
|
||||
|
||||
durationToRide = data[0]['duration']['value'];
|
||||
final points =
|
||||
decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinates.add(LatLng(lat, lng));
|
||||
final String pointsString =
|
||||
response['routes'][0]["overview_polyline"]["points"];
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < decodedPoints.length; i++) {
|
||||
// double lat = decodedPoints[i][0].toDouble();
|
||||
// double lng = decodedPoints[i][1].toDouble();
|
||||
polylineCoordinates.add(decodedPoints[i]);
|
||||
}
|
||||
// Define the northeast and southwest coordinates
|
||||
|
||||
@@ -4609,18 +4754,18 @@ class MapPassengerController extends GetxController {
|
||||
position: LatLng(
|
||||
data[0]["end_location"]['lat'], data[0]["end_location"]['lng']),
|
||||
icon: endIcon,
|
||||
// infoWindow: InfoWindow(
|
||||
// title: endNameAddress,
|
||||
// snippet:
|
||||
// '$distance ${'KM'.tr} ⌛ ${hours > 0 ? '${'Your Ride Duration is '.tr}$hours ${'H and'.tr} $minutes ${'m'.tr}' : '${'Your Ride Duration is '.tr} $minutes ${'m'.tr}'}'),
|
||||
infoWindow: InfoWindow(
|
||||
title: endNameAddress,
|
||||
snippet:
|
||||
'$distance ${'KM'.tr} ⌛ ${hours > 0 ? '${'Your Ride Duration is '.tr}$hours ${'H and'.tr} $minutes ${'m'.tr}' : '${'Your Ride Duration is '.tr} $minutes ${'m'.tr}'}'),
|
||||
),
|
||||
);
|
||||
// // Show info windows automatically
|
||||
// Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// mapController?.showMarkerInfoWindow(const MarkerId('start'));
|
||||
// });
|
||||
// Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// mapController?.showMarkerInfoWindow(const MarkerId('end'));
|
||||
// mapController?.showMarkerInfoWindow(const MarkerId('start'));
|
||||
// });
|
||||
// Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// mapController?.showMarkerInfoWindow(const MarkerId('end'));
|
||||
// });
|
||||
// update();
|
||||
|
||||
@@ -4632,11 +4777,23 @@ class MapPassengerController extends GetxController {
|
||||
false; // الأفضل أن يكون الافتراضي هو الجودة العالية
|
||||
|
||||
// نمرر عدد النقاط المناسب هنا
|
||||
animatePolylineLayered(
|
||||
polylineCoordinates,
|
||||
maxPoints:
|
||||
lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
animatePolylineLayered(
|
||||
polylineCoordinates,
|
||||
maxPoints:
|
||||
lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي
|
||||
);
|
||||
} else {
|
||||
polyLines.add(Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: polylineCoordinates,
|
||||
width: 6,
|
||||
color: AppColor.primaryColor,
|
||||
endCap: Cap.roundCap,
|
||||
startCap: Cap.roundCap,
|
||||
jointType: JointType.round,
|
||||
));
|
||||
}
|
||||
|
||||
rideConfirm = false;
|
||||
isMarkersShown = true;
|
||||
@@ -4672,7 +4829,7 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
// 2) رسم متدرّج بطبقات متراكبة (بدون حذف)، برونزي ↔ أخضر، مع zIndex وعرض مختلف
|
||||
Future<void> animatePolylineLayered(List<LatLng> coordinates,
|
||||
{int layersCount = 1, int stepDelayMs = 10, int maxPoints = 150}) async {
|
||||
{int layersCount = 8, int stepDelayMs = 10, int maxPoints = 160}) async {
|
||||
// امسح أي طبقات قديمة فقط الخاصة بالطريق
|
||||
polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_layer_'));
|
||||
update();
|
||||
@@ -4682,8 +4839,8 @@ class MapPassengerController extends GetxController {
|
||||
if (coords.length < 2) return;
|
||||
|
||||
// ألوان مع شفافية خفيفة للتمييز
|
||||
Color bronze([int alpha = 220]) => const Color(0xFFCD7F32).withAlpha(alpha);
|
||||
Color green([int alpha = 220]) => Colors.green.withAlpha(alpha);
|
||||
Color bronze([int alpha = 220]) => AppColor.gold;
|
||||
Color green([int alpha = 220]) => AppColor.primaryColor;
|
||||
|
||||
Color _layerColor(int layer) => (layer % 2 == 0) ? bronze() : green();
|
||||
|
||||
@@ -4818,14 +4975,16 @@ class MapPassengerController extends GetxController {
|
||||
distance = distanceOfDestination + (data[0]['distance']['value']) / 1000;
|
||||
|
||||
update();
|
||||
final points =
|
||||
decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
if (points[i][0].toString() != '') {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinatesPointsAll[index].add(LatLng(lat, lng));
|
||||
}
|
||||
// final points =
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
final String pointsString =
|
||||
response['routes'][0]["overview_polyline"]["points"];
|
||||
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < decodedPoints.length; i++) {
|
||||
polylineCoordinates.add(decodedPoints[i]);
|
||||
}
|
||||
// Define the northeast and southwest coordinates
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:Intaleq/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -7,22 +9,21 @@ import 'package:Intaleq/onbording_page.dart';
|
||||
import 'package:Intaleq/views/auth/login_page.dart';
|
||||
import 'package:Intaleq/controller/auth/login_controller.dart';
|
||||
import 'package:Intaleq/controller/functions/secure_storage.dart';
|
||||
import 'package:Intaleq/views/widgets/my_scafold.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import '../../../constant/notification.dart';
|
||||
import '../../../main.dart';
|
||||
import '../firebase/firbase_messge.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
// كنترولر مع منطق تحميل محسن ينتظر العمليات غير المتزامنة
|
||||
class SplashScreenController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
// الحركات الخاصة بكل عنصر من عناصر الواجهة
|
||||
late Animation<double> titleFadeAnimation;
|
||||
late Animation<double> titleScaleAnimation;
|
||||
late Animation<double> taglineFadeAnimation;
|
||||
late Animation<double> titleFadeAnimation,
|
||||
titleScaleAnimation,
|
||||
taglineFadeAnimation,
|
||||
footerFadeAnimation;
|
||||
late Animation<Offset> taglineSlideAnimation;
|
||||
late Animation<double> footerFadeAnimation;
|
||||
|
||||
final progress = 0.0.obs;
|
||||
Timer? _progressTimer;
|
||||
@@ -30,111 +31,160 @@ class SplashScreenController extends GetxController
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
vsync: this, duration: const Duration(milliseconds: 2000));
|
||||
|
||||
// --- تعريف الحركات المتتالية ---
|
||||
// Animation definitions
|
||||
titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
taglineSlideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut)));
|
||||
|
||||
_animationController.forward();
|
||||
|
||||
// بدء عملية التهيئة والتحميل
|
||||
_initializeApp();
|
||||
}
|
||||
|
||||
/// تهيئة التطبيق وانتظار انتهاء التحميل قبل الانتقال
|
||||
Future<void> _initializeApp() async {
|
||||
// تشغيل مؤقت شريط التقدم ليعطي انطباعاً مرئياً فقط
|
||||
_startProgressAnimation();
|
||||
|
||||
// تعريف مهمتين: منطق التحميل، والحد الأدنى لوقت عرض الشاشة
|
||||
// Run navigation logic and background services in parallel.
|
||||
final logicFuture = _performNavigationLogic();
|
||||
final backgroundServicesFuture = _initializeBackgroundServices();
|
||||
|
||||
// Ensure the splash screen is visible for a minimum duration.
|
||||
final minTimeFuture = Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
// الانتظار حتى انتهاء المهمتين معاً
|
||||
await Future.wait([logicFuture, minTimeFuture]);
|
||||
|
||||
// بعد انتهاء الانتظار، سيتم الانتقال تلقائياً من داخل a_performNavigationLogic
|
||||
// Wait for all tasks to complete.
|
||||
await Future.wait([logicFuture, backgroundServicesFuture, minTimeFuture]);
|
||||
}
|
||||
|
||||
/// تشغيل حركة شريط التقدم بشكل مرئي ومنفصل عن منطق التحميل
|
||||
void _startProgressAnimation() {
|
||||
const totalTime = 2800; // مدة ملء الشريط
|
||||
const interval = 50;
|
||||
// Visual timer for the progress bar.
|
||||
const totalTime = 2800; // ms
|
||||
const interval = 50; // ms
|
||||
int elapsed = 0;
|
||||
|
||||
_progressTimer =
|
||||
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||
elapsed += interval;
|
||||
progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
|
||||
if (elapsed >= totalTime) {
|
||||
timer.cancel();
|
||||
}
|
||||
if (elapsed >= totalTime) timer.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/// تنفيذ منطق التحميل الفعلي وتسجيل الدخول وتحديد وجهة الانتقال
|
||||
/// Initializes all heavy background services while the splash animation is running.
|
||||
Future<void> _initializeBackgroundServices() async {
|
||||
try {
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
// --- [LAZY LOADING IN ACTION] ---
|
||||
// This `Get.find()` call will create the NotificationController instance
|
||||
// for the first time because it was defined with `lazyPut`.
|
||||
final notificationController = Get.find<NotificationController>();
|
||||
await notificationController.initNotifications();
|
||||
|
||||
// The same happens for FirebaseMessagesController.
|
||||
final fcm = Get.find<FirebaseMessagesController>();
|
||||
await fcm.requestFirebaseMessagingPermission();
|
||||
|
||||
_scheduleDailyNotifications(notificationController);
|
||||
_initializeQuickActions();
|
||||
} catch (e, st) {
|
||||
CRUD.addError('background_init_error: $e', st.toString(), 'SplashScreen');
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the next screen based on user's login state.
|
||||
Future<void> _performNavigationLogic() async {
|
||||
// تنفيذ المهام الأولية
|
||||
await _getPackageInfo();
|
||||
SecureStorage().saveData('iss', 'mobile-app:');
|
||||
|
||||
// تحديد الشاشة التالية
|
||||
if (box.read(BoxName.onBoarding) == null) {
|
||||
Get.off(() => OnBoardingPage());
|
||||
} else if (box.read(BoxName.email) != null &&
|
||||
box.read(BoxName.phone) != null &&
|
||||
box.read(BoxName.isVerified) == '1') {
|
||||
// -- النقطة الأهم --
|
||||
// هنا ننتظر انتهاء عملية تسجيل الدخول قبل الانتقال
|
||||
await Get.put(LoginController()).loginUsingCredentials(
|
||||
// `Get.find()` creates the LoginController instance here.
|
||||
final loginController = Get.find<LoginController>();
|
||||
// The loginController itself will handle navigation via Get.offAll() upon success.
|
||||
await loginController.loginUsingCredentials(
|
||||
box.read(BoxName.passengerID).toString(),
|
||||
box.read(BoxName.email).toString(),
|
||||
);
|
||||
// بعد هذه العملية، سيتولى LoginController بنفسه الانتقال للصفحة الرئيسية أو صفحة الدخول
|
||||
} else {
|
||||
Get.off(() => LoginPage());
|
||||
}
|
||||
}
|
||||
|
||||
/// جلب معلومات الحزمة لعرض إصدار التطبيق
|
||||
Future<void> _getPackageInfo() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
box.write(BoxName.packagInfo, info.version);
|
||||
update();
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
box.write(BoxName.packagInfo, info.version);
|
||||
update();
|
||||
} catch (e) {
|
||||
print("Could not get package info: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleDailyNotifications(NotificationController controller) {
|
||||
try {
|
||||
final List<String> msgs = passengerMessages ?? const [];
|
||||
if (msgs.isEmpty) {
|
||||
controller.scheduleNotificationsForSevenDays(
|
||||
'Intaleq', 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.', "tone1");
|
||||
} else {
|
||||
final rnd = Random();
|
||||
final raw = msgs[rnd.nextInt(msgs.length)];
|
||||
final parts = raw.split(':');
|
||||
final title = parts.isNotEmpty ? parts.first.trim() : 'Intaleq';
|
||||
final body = parts.length > 1
|
||||
? parts.sublist(1).join(':').trim()
|
||||
: 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
|
||||
controller.scheduleNotificationsForSevenDays(
|
||||
title.isEmpty ? 'Intaleq' : title,
|
||||
body.isEmpty ? 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.' : body,
|
||||
"tone1");
|
||||
}
|
||||
} catch (e, st) {
|
||||
CRUD.addError('notif_init: $e', st.toString(), 'SplashScreen');
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeQuickActions() {
|
||||
final QuickActions quickActions = QuickActions();
|
||||
quickActions.initialize((String shortcutType) {
|
||||
Get.toNamed('/$shortcutType');
|
||||
});
|
||||
|
||||
quickActions.setShortcutItems(<ShortcutItem>[
|
||||
ShortcutItem(
|
||||
type: 'shareApp', localizedTitle: 'Share App'.tr, icon: 'icon_share'),
|
||||
ShortcutItem(
|
||||
type: 'wallet', localizedTitle: 'Wallet'.tr, icon: 'icon_wallet'),
|
||||
ShortcutItem(
|
||||
type: 'profile', localizedTitle: 'Profile'.tr, icon: 'icon_user'),
|
||||
ShortcutItem(
|
||||
type: 'contactSupport',
|
||||
localizedTitle: 'Contact Support'.tr,
|
||||
icon: 'icon_support'),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -144,40 +194,3 @@ class SplashScreenController extends GetxController
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
// يمكن الإبقاء على هذه الفئة لواجهة المستخدم المتعلقة بالأمان كما في الملف الأصلي
|
||||
class SecurityPage extends StatelessWidget {
|
||||
const SecurityPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: "security_warning".tr,
|
||||
body: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"security_message".tr,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// await SecurityHelper.clearAllData();
|
||||
},
|
||||
child: Text(
|
||||
"security_warning".tr,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user