import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:intaleq_maps/intaleq_maps.dart'; import '../config/app_config.dart'; import '../models/landmark.dart'; import 'offline_los_engine.dart'; class TacticalRouteResult { final List points; final double distanceKm; final double durationMinutes; final String profile; TacticalRouteResult({ required this.points, required this.distanceKm, required this.durationMinutes, required this.profile, }); } class TacticalApiService { static String get defaultServerUrl => AppConfig.serverUrl; static String get defaultApiKey => AppConfig.apiKey; /// Calculate tactical route between waypoints using server routing engine static Future calculateTacticalRoute({ required LatLng origin, required LatLng destination, String profile = 'car', // 'car', 'offroad', 'convoy', 'emergency' String? serverUrl, String? apiKey, }) async { final url = serverUrl ?? defaultServerUrl; final key = apiKey ?? defaultApiKey; try { final serverProfile = (profile == 'convoy' || profile == 'armored' || profile == 'offroad4x4' || profile == 'rapidResponse') ? 'car' : profile; final uri = Uri.parse( '$url/api/maps/route?fromLat=${origin.latitude}&fromLng=${origin.longitude}' '&toLat=${destination.latitude}&toLng=${destination.longitude}' '&profile=$serverProfile&steps=true&locale=ar', ); final resp = await http.get(uri, headers: {'x-api-key': key}).timeout(const Duration(seconds: 10)); if (resp.statusCode == 200) { final data = jsonDecode(resp.body); double distanceMeters = 0.0; double durationSec = 0.0; final coords = []; // 1. Direct NestJS Map-SaaS API response format if (data['distance'] != null) { distanceMeters = (data['distance'] as num?)?.toDouble() ?? 0.0; durationSec = (data['duration'] as num?)?.toDouble() ?? 0.0; // Adjust tactical convoy duration if (profile == 'convoy') durationSec *= 1.45; if (profile == 'armored') durationSec *= 1.75; if (profile == 'offroad4x4') durationSec *= 1.1; if (profile == 'rapidResponse') durationSec *= 0.85; if (data['points'] is String) { coords.addAll(PolylineUtils.decode(data['points'] as String)); } else if (data['points'] is List) { for (final pt in data['points']) { if (pt is List && pt.length >= 2) { coords.add(LatLng((pt[1] as num).toDouble(), (pt[0] as num).toDouble())); } } } else if (data['points'] is Map && data['points']['coordinates'] != null) { final List coordList = data['points']['coordinates']; for (final c in coordList) { coords.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble())); } } } // 2. Fallback to raw / paths GraphHopper format else if (data['paths'] != null && (data['paths'] as List).isNotEmpty) { final path = data['paths'][0]; distanceMeters = (path['distance'] as num?)?.toDouble() ?? 0.0; final timeMs = (path['time'] as num?)?.toDouble() ?? 0.0; durationSec = timeMs / 1000.0; if (path['points'] is String) { coords.addAll(PolylineUtils.decode(path['points'] as String)); } else if (path['points'] is Map && path['points']['coordinates'] != null) { final List coordList = path['points']['coordinates']; for (final c in coordList) { coords.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble())); } } } if (coords.isNotEmpty) { return TacticalRouteResult( points: coords, distanceKm: distanceMeters / 1000.0, durationMinutes: durationSec / 60.0, profile: profile, ); } } } catch (e) { debugPrint('Tactical route API error: $e'); } return null; } /// Search places via Server Geocoding API (Forward Geocoding & POI Search) static Future> searchOnlinePlaces(String query, {String? region}) async { final cleanQuery = query.trim(); if (cleanQuery.isEmpty) return []; try { final uri = Uri.parse( '$defaultServerUrl/api/geocoding/search?q=${Uri.encodeComponent(cleanQuery)}&country=jordan', ); final resp = await http.get(uri, headers: {'x-api-key': defaultApiKey}).timeout(const Duration(seconds: 4)); if (resp.statusCode == 200) { final data = jsonDecode(resp.body); final list = (data is List) ? data : (data['results'] is List ? data['results'] as List : []); return list.map((item) { final name = item['name_ar'] ?? item['name'] ?? cleanQuery; final cat = (item['category'] ?? item['type'] ?? '').toString().toLowerCase(); LandmarkType type = LandmarkType.tower; if (cat.contains('worship') || cat.contains('mosque') || name.contains('مسجد') || name.contains('جامع')) { type = LandmarkType.minaret; } else if (cat.contains('water') || name.contains('خزان') || name.contains('بركة')) { type = LandmarkType.waterTank; } else if (cat.contains('fort') || cat.contains('castle') || name.contains('قلعة') || name.contains('قصر')) { type = LandmarkType.fort; } else if (cat.contains('mountain') || cat.contains('peak') || name.contains('جبل') || name.contains('تل')) { type = LandmarkType.mountain; } final lat = (item['latitude'] ?? item['lat'] as num?)?.toDouble() ?? 32.0; final lng = (item['longitude'] ?? item['lng'] as num?)?.toDouble() ?? 36.0; final regionName = item['governorate'] ?? item['district'] ?? item['city'] ?? 'الأردن'; return TacticalLandmark( id: 'geo-${item['id'] ?? lat}_$lng', name: name, region: regionName, type: type, lat: lat, lng: lng, elevationM: (item['elevation'] as num?)?.toInt() ?? 750, description: item['neighbourhood'] ?? item['formatted_address'] ?? '', ); }).toList(); } } catch (e) { debugPrint('Online geocoding search error: $e'); } return []; } /// Calculate Tactical Line of Sight (LOS) between Observer and Target static Future?> calculateLineOfSight({ required LatLng observer, required LatLng target, double observerHeight = 2.0, double targetHeight = 2.0, String? serverUrl, String? apiKey, }) async { final url = serverUrl ?? defaultServerUrl; final key = apiKey ?? defaultApiKey; try { final uri = Uri.parse( '$url/api/tactical/line-of-sight?observerLat=${observer.latitude}&observerLng=${observer.longitude}' '&targetLat=${target.latitude}&targetLng=${target.longitude}' '&observerHeight=$observerHeight&targetHeight=$targetHeight', ); final resp = await http.get(uri, headers: {'x-api-key': key}).timeout(const Duration(seconds: 8)); if (resp.statusCode == 200) { return jsonDecode(resp.body); } } catch (e) { debugPrint('Tactical LOS API error: $e'); } return null; } /// Sovereign Line of Sight solver with zero latency and full offline capability static Future calculateHybridLineOfSight({ required LatLng observer, required LatLng target, double observerHeight = 2.0, double targetHeight = 2.0, String? serverUrl, String? apiKey, }) async { return OfflineLosEngine.calculateAsync( observer: observer, target: target, observerHeightM: observerHeight, targetHeightM: targetHeight, ); } /// Calculate Tactical Isochrone Reachability Polygons static Future?> calculateIsochrone({ required LatLng center, List timeBuckets = const [300, 600, 900], // 5, 10, 15 minutes String? serverUrl, String? apiKey, }) async { final url = serverUrl ?? defaultServerUrl; final key = apiKey ?? defaultApiKey; try { final timesStr = timeBuckets.join(','); final uri = Uri.parse( '$url/api/tactical/isochrone?lat=${center.latitude}&lng=${center.longitude}×=$timesStr&profile=emergency', ); final resp = await http.get(uri, headers: {'x-api-key': key}).timeout(const Duration(seconds: 10)); if (resp.statusCode == 200) { return jsonDecode(resp.body); } } catch (e) { debugPrint('Tactical Isochrone API error: $e'); } return null; } }