import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart'; import 'package:wanisclient/config/constants/app_constant.dart'; import 'package:wanisclient/config/custom_text.dart'; import 'package:wanisclient/providers/language_provider.dart'; import 'package:wanisclient/utils/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:provider/provider.dart'; import '../providers/ride_providers.dart'; /*─────────────────────────────────────────────────────────────*/ /* NOTE: keep mapBoxApikey in app_constant.dart */ /*─────────────────────────────────────────────────────────────*/ class map_screen extends StatefulWidget { final void Function(LatLng?) on_location_selected; const map_screen({Key? key, required this.on_location_selected}) : super(key: key); @override State createState() => _map_screen_state(); } /*─────────────────────────────────────────────────────────────*/ class _map_screen_state extends State { /* ――― controllers ――― */ MapboxMap? _mapboxController; final TextEditingController _search_controller = TextEditingController(); late final RidesProvider _ridesProvider; /* ――― positions ――― */ // Using Sousse as the fallback position based on the context provided static const LatLng _fallback_position = LatLng(35.8256, 10.6369); // Sousse coordinates LatLng? _initial_position; // set after GPS LatLng _marker_position = _fallback_position; /* ――― UI state ――― */ List> _place_predictions = []; PointAnnotationManager? _pointAnnotationManager; /*───────────────── lifecycle ─────────────────*/ @override void initState() { super.initState(); // Initialize with fallback position immediately so the map can render. _ridesProvider = context.read(); _initial_position = _fallback_position; _marker_position = _fallback_position; } @override void dispose() { _search_controller.dispose(); super.dispose(); } /*───────────────── current location ─────────────────*/ Future _get_user_location() async { final location = Location(); bool serviceEnabled; PermissionStatus permissionGranted; // Check if location service is enabled serviceEnabled = await location.serviceEnabled(); if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) { _handleLocationError("Location service not enabled."); return; } } // Check and request permission permissionGranted = await location.requestPermission(); if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); if (permissionGranted != PermissionStatus.granted) { _handleLocationError("Location permission denied."); return; } } if (permissionGranted != PermissionStatus.granted) { _handleLocationError("Location permission denied."); return; } LocationData loc; try { loc = await location.getLocation(); } catch (e) { _handleLocationError("Failed to get location: $e"); return; } if (!mounted) return; final pos = LatLng( loc.latitude ?? _fallback_position.latitude, loc.longitude ?? _fallback_position.longitude, ); setState(() { _initial_position = pos; _marker_position = pos; }); // Animate camera to the new position if (_mapboxController != null) { await _mapboxController!.easeTo( CameraOptions( center: Point(coordinates: Position(pos.longitude, pos.latitude)), zoom: 15.0, ), MapAnimationOptions(duration: 1000), ); await _updateMarker(pos); } } void _handleLocationError(String message) { debugPrint("Location Error: $message"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( languageSingleton.translate( 'Failed to get your location. Displaying default location.'), ), duration: const Duration(seconds: 3), ), ); setState(() { _initial_position = _fallback_position; _marker_position = _fallback_position; }); if (_mapboxController != null) { _mapboxController!.easeTo( CameraOptions( center: Point(coordinates: Position(_initial_position!.longitude, _initial_position!.latitude)), zoom: 10.0, ), MapAnimationOptions(duration: 1000), ); } } } /*───────────────── map helpers ─────────────────*/ Future _on_map_created(MapboxMap mapboxMap) async { _mapboxController = mapboxMap; _pointAnnotationManager = await mapboxMap.annotations.createPointAnnotationManager(); // Get user location after map is ready await _get_user_location(); await _updateMarker(_marker_position); // Add tap listener for map - using the correct API for v2.10.0 mapboxMap.setOnMapTapListener((context) { final coordinate = context.point; final newPos = LatLng(coordinate.coordinates.lat.toDouble(), coordinate.coordinates.lng.toDouble()); setState(() => _marker_position = newPos); _updateMarker(newPos); }); } Future _updateMarker(LatLng position) async { if (_pointAnnotationManager == null) return; // Clear existing annotations await _pointAnnotationManager!.deleteAll(); // Create marker image from asset final ByteData data = await rootBundle.load('assets/icons/pin-map.png'); final Uint8List bytes = data.buffer.asUint8List(); // Create point annotation final pointAnnotationOptions = PointAnnotationOptions( geometry: Point(coordinates: Position(position.longitude, position.latitude)), image: bytes, iconSize: 0.5, // Adjust size as needed ); await _pointAnnotationManager!.create(pointAnnotationOptions); } Future _move_camera(LatLng pos) async { if (_mapboxController == null) return; await _mapboxController!.easeTo( CameraOptions( center: Point(coordinates: Position(pos.longitude, pos.latitude)), zoom: 15.0, ), MapAnimationOptions(duration: 1000), ); } /*───────────────── Mapbox Places REST ─────────────────*/ Future _search_placesGoogle(String input) async { if (input.trim().length < 3) { setState(() => _place_predictions.clear()); return; } final results = await _ridesProvider.searchPlaces(input); setState(() { _place_predictions = results; }); } Future _select_placeGoogle(String placeId) async { final LatLng? pos = await _ridesProvider.getPlaceCoordinates(placeId); if (pos == null) return; setState(() { _marker_position = pos; _search_controller.clear(); _place_predictions.clear(); }); await _move_camera(pos); await _updateMarker(pos); } /*───────────────── build ─────────────────*/ @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ /* map layer */ Positioned.fill( child: MapWidget( key: ValueKey("mapWidget"), cameraOptions: CameraOptions( center: Point(coordinates: Position(_initial_position!.longitude, _initial_position!.latitude)), zoom: 10.0, ), styleUri: MapboxStyles.MAPBOX_STREETS, textureView: true, // Better performance on some devices onMapCreated: _on_map_created, ), ), /* search bar */ Positioned( top: MediaQuery.of(context).padding.top + 10, left: 16, right: 16, child: Material( elevation: 4, borderRadius: BorderRadius.circular(25), child: TextField( controller: _search_controller, onChanged: _search_placesGoogle, decoration: InputDecoration( hintText: languageSingleton.translate('search location'), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: () { _search_controller.clear(); setState(() => _place_predictions.clear()); }, ), ), ), ), ), /* predictions dropdown */ if (_place_predictions.isNotEmpty) Positioned( top: MediaQuery.of(context).padding.top + 66, left: 16, right: 16, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 260), child: Card( margin: EdgeInsets.zero, child: ListView.separated( padding: EdgeInsets.zero, itemCount: _place_predictions.length, separatorBuilder: (_, __) => const Divider(height: 1, color: Colors.grey), itemBuilder: (ctx, i) { final p = _place_predictions[i]; return ListTile( dense: true, leading: const Icon(Icons.location_on, color: AppColors.primaryColor), title: Text(p['description'] ?? ''), onTap: () => _select_placeGoogle(p['place_id']), ); }, ), ), ), ), /* current-loc FAB */ Positioned( bottom: 120, right: 16, child: FloatingActionButton( heroTag: 'loc_btn', backgroundColor: AppColors.primaryColor, onPressed: _get_user_location, child: const Icon(Icons.my_location, color: Colors.white), ), ), /* cancel / select buttons */ Positioned( bottom: 20, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _actionButton('Cancel', AppColors.secondaryColor, () { Navigator.pop(context); }), _actionButton('Select', AppColors.primaryColor, () { widget.on_location_selected(_marker_position); Navigator.pop(context); }), ], ), ), ], ), ); } Widget _actionButton(String label, Color color, VoidCallback onTap) { return SizedBox( width: MediaQuery.of(context).size.width * 0.35, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: color, padding: const EdgeInsets.all(12), ), onPressed: onTap, child: Txt(text: label, style: const TextStyle(color: Colors.white)), ), ); } }