feat: add device authentication, place gates support, and PiP navigation features
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
class PlaceGate {
|
||||
final String nameAr;
|
||||
final String? nameEn;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final bool isMainGate;
|
||||
|
||||
const PlaceGate({
|
||||
required this.nameAr,
|
||||
this.nameEn,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.isMainGate = false,
|
||||
});
|
||||
|
||||
factory PlaceGate.fromJson(Map<String, dynamic> json) {
|
||||
return PlaceGate(
|
||||
nameAr: json['name_ar']?.toString() ?? json['gate_name_ar']?.toString() ?? 'بوابة',
|
||||
nameEn: json['name_en']?.toString() ?? json['gate_name_en']?.toString(),
|
||||
latitude: (json['latitude'] as num?)?.toDouble() ??
|
||||
(json['lat'] as num?)?.toDouble() ??
|
||||
double.tryParse(json['latitude']?.toString() ?? '0.0') ??
|
||||
0.0,
|
||||
longitude: (json['longitude'] as num?)?.toDouble() ??
|
||||
(json['lng'] as num?)?.toDouble() ??
|
||||
double.tryParse(json['longitude']?.toString() ?? '0.0') ??
|
||||
0.0,
|
||||
isMainGate: json['is_main_gate'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name_ar': nameAr,
|
||||
if (nameEn != null) 'name_en': nameEn,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'is_main_gate': isMainGate,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'place_gate.dart';
|
||||
|
||||
class PlaceModel {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -7,6 +9,7 @@ class PlaceModel {
|
||||
final double elevationMeters; // GPS Altitude AMSL in meters (defaults to 0.0)
|
||||
final double? distanceKm;
|
||||
final String? address;
|
||||
final List<PlaceGate> gates;
|
||||
|
||||
PlaceModel({
|
||||
required this.id,
|
||||
@@ -17,8 +20,11 @@ class PlaceModel {
|
||||
this.elevationMeters = 0.0,
|
||||
this.distanceKm,
|
||||
this.address,
|
||||
this.gates = const [],
|
||||
});
|
||||
|
||||
bool get hasGates => gates.isNotEmpty;
|
||||
|
||||
factory PlaceModel.fromJson(Map<String, dynamic> json) {
|
||||
final rawElev = json['elevation_meters'] ?? json['elevation'] ?? json['altitude'] ?? 0.0;
|
||||
double elev = 0.0;
|
||||
@@ -28,6 +34,14 @@ class PlaceModel {
|
||||
elev = double.tryParse(rawElev.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
final rawGates = json['gates'];
|
||||
List<PlaceGate> parsedGates = [];
|
||||
if (rawGates is List) {
|
||||
parsedGates = rawGates
|
||||
.map((g) => PlaceGate.fromJson(Map<String, dynamic>.from(g)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return PlaceModel(
|
||||
id: json['id']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
@@ -39,6 +53,7 @@ class PlaceModel {
|
||||
? (json['distanceKm'] as num).toDouble()
|
||||
: null,
|
||||
address: json['address']?.toString() ?? json['neighborhood']?.toString(),
|
||||
gates: parsedGates,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,6 +67,7 @@ class PlaceModel {
|
||||
'elevation_meters': elevationMeters,
|
||||
'altitude': elevationMeters,
|
||||
if (address != null) 'address': address,
|
||||
if (gates.isNotEmpty) 'gates': gates.map((g) => g.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../../core/constants/api_constants.dart';
|
||||
import '../../core/services/device_fingerprint_service.dart';
|
||||
import '../../core/utils/polyline_decoder.dart';
|
||||
import '../../core/utils/arabic_search_normalizer.dart';
|
||||
import '../../core/services/location_service.dart';
|
||||
@@ -15,6 +16,12 @@ class MapSaasRepository {
|
||||
|
||||
MapSaasRepository({http.Client? client}) : client = client ?? http.Client();
|
||||
|
||||
Map<String, String> get _baseHeaders => {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': DeviceFingerprintService.instance.activeApiKey,
|
||||
...DeviceFingerprintService.instance.headers,
|
||||
};
|
||||
|
||||
/// Fetch primary and alternative routes from MapSaaS
|
||||
Future<List<RouteData>> getRoute({
|
||||
required LatLng origin,
|
||||
@@ -43,7 +50,7 @@ class MapSaasRepository {
|
||||
try {
|
||||
final response = await client.get(
|
||||
saasUri,
|
||||
headers: {'x-api-key': ApiConstants.mapSaasKey},
|
||||
headers: _baseHeaders,
|
||||
);
|
||||
print("📥 [MapSaasRepo] Route response HTTP status: ${response.statusCode} (${response.body.length} bytes)");
|
||||
|
||||
@@ -165,7 +172,7 @@ class MapSaasRepository {
|
||||
final uri = Uri.parse(ApiConstants.mapSaasSearch).replace(queryParameters: queryParams);
|
||||
final response = await client.get(
|
||||
uri,
|
||||
headers: {'x-api-key': ApiConstants.mapSaasKey},
|
||||
headers: _baseHeaders,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -232,14 +239,12 @@ class MapSaasRepository {
|
||||
return distM <= ApiConstants.maxSearchRadiusMeters;
|
||||
}).toList();
|
||||
|
||||
// 4. Smart Ranking: exact text match priority + proximity ascending
|
||||
final bool categorySearch = ArabicSearchNormalizer.isCategoryQuery(query);
|
||||
|
||||
// 4. Smart Ranking:
|
||||
// - For category searches (e.g. مطعم, مخبز, دكان, مسجد, سوبرماركت), PROXIMITY dominates (closest first).
|
||||
// - For landmark/name searches (e.g. المدينة الطبية, سيتي مول), exact name match takes precedence, then proximity.
|
||||
filteredPlaces.sort((a, b) {
|
||||
final matchA = ArabicSearchNormalizer.matches(a.name, normalizedQuery);
|
||||
final matchB = ArabicSearchNormalizer.matches(b.name, normalizedQuery);
|
||||
|
||||
if (matchA && !matchB) return -1;
|
||||
if (!matchA && matchB) return 1;
|
||||
|
||||
final distA = LocationService.instance.calculateDistance(
|
||||
center,
|
||||
LatLng(a.latitude, a.longitude),
|
||||
@@ -248,6 +253,17 @@ class MapSaasRepository {
|
||||
center,
|
||||
LatLng(b.latitude, b.longitude),
|
||||
);
|
||||
|
||||
if (categorySearch) {
|
||||
return distA.compareTo(distB);
|
||||
}
|
||||
|
||||
final matchA = ArabicSearchNormalizer.matches(a.name, normalizedQuery);
|
||||
final matchB = ArabicSearchNormalizer.matches(b.name, normalizedQuery);
|
||||
|
||||
if (matchA && !matchB) return -1;
|
||||
if (!matchA && matchB) return 1;
|
||||
|
||||
return distA.compareTo(distB);
|
||||
});
|
||||
|
||||
@@ -267,7 +283,7 @@ class MapSaasRepository {
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: {
|
||||
'x-api-key': ApiConstants.mapSaasKey,
|
||||
..._baseHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
@@ -308,15 +324,15 @@ class MapSaasRepository {
|
||||
'driver_id': driverId,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'speed': speed,
|
||||
'heading': heading,
|
||||
'speed': speed < 0 ? 0.0 : speed,
|
||||
'heading': heading < 0 ? 0.0 : heading,
|
||||
'distance': distance,
|
||||
'elevation': elevation,
|
||||
};
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: {
|
||||
'x-api-key': ApiConstants.mapSaasKey,
|
||||
..._baseHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
|
||||
Reference in New Issue
Block a user