import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:location/location.dart'; import 'package:latlong2/latlong.dart'; import 'package:lba/res/colors.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lba/gen/assets.gen.dart'; import 'package:lba/widgets/app_snackbar.dart'; import 'dart:math' as math; class LocationRadiusScreen extends StatefulWidget { const LocationRadiusScreen({super.key}); @override State createState() => _LocationRadiusScreenState(); } class _LocationRadiusScreenState extends State with TickerProviderStateMixin { final MapController _mapController = MapController(); LatLng _currentCenter = const LatLng(25.1972, 55.2744); // Default: Dubai double _radiusInKm = 1.0; bool _isCustomRadius = false; bool _isLoadingLocation = true; bool _mapLoadError = false; int _currentTileProvider = 0; Location location = Location(); late bool _serviceEnabled; late PermissionStatus _permissionGranted; late LocationData _locationData; late AnimationController _animationController; late List> _animations; final List> _tileProviders = [ { 'name': 'Stamen Terrain', 'lightUrl': 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png', 'darkUrl': 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}{r}.png', 'subdomains': ['a', 'b', 'c', 'd'], }, { 'name': 'CartoDB', 'lightUrl': 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', 'darkUrl': 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', 'subdomains': ['a', 'b', 'c', 'd'], }, { 'name': 'Google Maps', 'lightUrl': 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', 'darkUrl': 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', 'subdomains': ['mt0', 'mt1', 'mt2', 'mt3'], }, ]; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), ); _animations = List.generate(6, (index) { final double startTime = (index / 6) * 0.5; final double endTime = (startTime + 0.6).clamp(0.0, 1.0); return Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: Interval(startTime, endTime, curve: Curves.easeOutCubic), ), ); }); _currentTileProvider = _currentTileProvider.clamp(0, _tileProviders.length - 1); _determinePosition(); } @override void dispose() { _animationController.dispose(); super.dispose(); } Future _determinePosition() async { setState(() => _isLoadingLocation = true); try { _serviceEnabled = await location.serviceEnabled(); if (!_serviceEnabled) { _serviceEnabled = await location.requestService(); if (!_serviceEnabled) { _showLocationMessage( 'Location services are disabled. Please enable it in settings.', type: SnackBarType.warning); setState(() => _isLoadingLocation = false); _animationController.forward(); return; } } _permissionGranted = await location.hasPermission(); if (_permissionGranted == PermissionStatus.denied) { _permissionGranted = await location.requestPermission(); if (_permissionGranted != PermissionStatus.granted) { _showLocationMessage('Location permissions are denied.', type: SnackBarType.error); setState(() => _isLoadingLocation = false); _animationController.forward(); return; } } _locationData = await location.getLocation(); setState(() { _currentCenter = LatLng(_locationData.latitude!, _locationData.longitude!); _isLoadingLocation = false; }); _showLocationMessage('Location found successfully!', type: SnackBarType.success); _mapController.move(_currentCenter, 14.0); } catch (e) { _showLocationMessage('Unable to get your location. Using default location.', type: SnackBarType.info); setState(() { _isLoadingLocation = false; }); } finally { if (mounted) { _animationController.forward(); } } } void _showLocationMessage(String message, {SnackBarType type = SnackBarType.error}) { if (mounted) { AppSnackBar.show( context: context, message: message, type: type, duration: const Duration(seconds: 3), ); } } Widget _buildAnimatedWidget(Widget child, int index) { return FadeTransition( opacity: _animations[index], child: SlideTransition( position: Tween( begin: const Offset(0.0, 0.5), end: Offset.zero, ).animate(_animations[index]), child: child, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.scaffoldBackground, appBar: AppBar( title: Text( "Choose a location to see what's available", style: TextStyle( color: AppColors.textPrimary, fontWeight: FontWeight.normal, fontSize: 15), ), backgroundColor: AppColors.surface, elevation: 0, leading: IconButton( icon: SvgPicture.asset( Assets.icons.arrowLeft.path, color: AppColors.textPrimary, ), onPressed: () => Navigator.of(context).pop(), ), ), body: Stack( children: [ _mapLoadError ? _buildMapErrorFallback() : Container( padding: const EdgeInsets.only(bottom: 240), child: FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _currentCenter, initialZoom: 14.0, onPositionChanged: (position, hasGesture) { if (hasGesture) { setState(() { _currentCenter = position.center; }); } }, ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', userAgentPackageName: 'com.example.lba', subdomains: const ['a', 'b', 'c', 'd'], additionalOptions: const { 'attribution': '© CARTO © OpenStreetMap contributors', }, ), CircleLayer( circles: [ CircleMarker( point: _currentCenter, radius: _radiusInKm * 1000, useRadiusInMeter: true, color: _isCustomRadius ? AppColors.primary.withOpacity(0.15) : AppColors.offerTimer.withOpacity(0.15), borderColor: Colors.transparent, borderStrokeWidth: 0, ), ], ), MarkerLayer( markers: [ Marker( width: _calculateCirclePixelSize(), height: _calculateCirclePixelSize(), point: _currentCenter, child: CustomPaint( painter: DashedCirclePainter( color: _isCustomRadius ? AppColors.primary : AppColors.offerTimer, strokeWidth: 3.0, dashLength: 9.0, gapLength: 5.0, ), ), ), ], ), ], ), ), if (_isLoadingLocation) Container( color: AppColors.scaffoldBackground.withOpacity(0.8), child: Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppColors.primary), ), ), ), _buildControlsPanel(), ], ), ); } Widget _buildMapErrorFallback() { return Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( color: AppColors.cardBackground, border: Border.all(color: AppColors.divider), ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 40), Container( width: 120, height: 120, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: _isCustomRadius ? AppColors.primary : AppColors.offerTimer, width: 4, ), color: (_isCustomRadius ? AppColors.primary : AppColors.offerTimer) .withOpacity(0.08), boxShadow: [ BoxShadow( color: AppColors.shadowColor, blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: _isCustomRadius ? AppColors.primary : AppColors.offerTimer, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColors.shadowColor, blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon( Icons.location_on, color: AppColors.surface, size: 24, ), ), const SizedBox(height: 8), Text( '${_radiusInKm.toStringAsFixed(1)} km', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), Text( 'radius', style: TextStyle( fontSize: 10, color: AppColors.textSecondary, ), ), ], ), ), ), const Spacer(), ], ), ); } Widget _buildControlsPanel() { return Positioned( bottom: 0, left: 0, right: 0, child: SlideTransition( position: Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, )), child: Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: AppColors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow( color: AppColors.shadowColor, blurRadius: 10, offset: const Offset(0, -2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildAnimatedWidget( Text( "Preset Radius Cards", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.nearbyPopuphint, ), ), 0, ), const SizedBox(height: 8), if (_mapLoadError) _buildAnimatedWidget( Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.errorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: AppColors.errorColor.withOpacity(0.3), ), ), child: Row( children: [ Icon( Icons.warning_amber_rounded, color: AppColors.errorColor, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( 'Map tiles blocked. Using fallback display.', style: TextStyle( color: AppColors.errorColor, fontSize: 12, ), ), ), ], ), ), 1, ), const SizedBox(height: 12), _buildAnimatedWidget( Row( children: [ _buildRadiusToggleChip( "Nearby (1 km)", !_isCustomRadius, () { setState(() { _isCustomRadius = false; _radiusInKm = 1.0; }); }), const SizedBox(width: 8), _buildRadiusToggleChip("Custom Radius", _isCustomRadius, () { setState(() { _isCustomRadius = true; if (_radiusInKm == 1.0) _radiusInKm = 5.0; }); }), ], ), 2, ), const SizedBox(height: 16), _buildAnimatedWidget( Row( children: [ Expanded( child: Slider( value: _radiusInKm, min: 1.0, max: 25.0, divisions: 24, activeColor: _isCustomRadius ? AppColors.primary : AppColors.divider, inactiveColor: AppColors.divider.withOpacity(0.5), onChanged: _isCustomRadius ? (value) { setState(() { _radiusInKm = value; }); } : null, ), ), const SizedBox(width: 16), Text( "${_radiusInKm.toStringAsFixed(1)} km", style: TextStyle( fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), ], ), 3, ), _buildAnimatedWidget( Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton.icon( onPressed: _determinePosition, icon: SvgPicture.asset( Assets.icons.currentLoc.path, color: AppColors.primary, ), label: Text( "Use my current location", style: TextStyle( color: AppColors.primary, fontWeight: FontWeight.bold), ), ), ], ), ), 4, ), const SizedBox(height: 8), _buildAnimatedWidget( SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () { Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.confirmButton, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(27), ), ), child: Text( 'Apply', style: TextStyle( color: AppColors.surface, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), 5, ), const SizedBox( height: 30, ) ], ), ), ), ); } Widget _buildRadiusToggleChip( String label, bool isSelected, VoidCallback onTap) { return ChoiceChip( label: Text(label), selected: isSelected, onSelected: (selected) { if (selected) onTap(); }, selectedColor: isSelected && !_isCustomRadius ? AppColors.primary : AppColors.primary, backgroundColor: AppColors.cardBackground, labelStyle: TextStyle( color: isSelected ? AppColors.nearbyPopup : AppColors.textSecondary, fontWeight: FontWeight.bold, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( color: isSelected ? Colors.transparent : AppColors.divider, ), ), showCheckmark: false, ); } double _calculateCirclePixelSize() { try { final double currentZoom = _mapController.camera.zoom; final double pixelsPerMeter = 256 * math.pow(2, currentZoom) / (2 * math.pi * 6378137 * math.cos(_currentCenter.latitude * math.pi / 180)); final double radiusInMeters = _radiusInKm * 1000; final double radiusInPixels = radiusInMeters * pixelsPerMeter; return radiusInPixels * 2; } catch (e) { const double pixelsPerKmAtZoom14 = 100.0; return _radiusInKm * pixelsPerKmAtZoom14 * 2; } } } class DashedCirclePainter extends CustomPainter { final Color color; final double strokeWidth; final double dashLength; final double gapLength; DashedCirclePainter({ required this.color, required this.strokeWidth, required this.dashLength, required this.gapLength, }); @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = color ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; final double radius = (size.width - strokeWidth) / 2; final Offset center = Offset(size.width / 2, size.height / 2); final double circumference = 2 * math.pi * radius; final double totalDashLength = dashLength + gapLength; final int dashCount = (circumference / totalDashLength).floor(); for (int i = 0; i < dashCount; i++) { final double startAngle = (i * totalDashLength / radius); final double sweepAngle = dashLength / radius; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint, ); } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }