dzocesrce

Untitled

Apr 8th, 2026
127
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.19 KB | None | 0 0
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:google_maps_flutter/google_maps_flutter.dart';
  4. import 'package:provider/provider.dart';
  5.  
  6. import '../../app/theme/theme_provider.dart';
  7. import '../../core/utils/app_error_messages.dart';
  8. import '../../models/location_model.dart';
  9. import '../../providers/location_provider.dart';
  10. import '../../providers/map_provider.dart';
  11. import '../../widgets/common/app_error_state.dart';
  12. import 'widgets/map_filters_button.dart';
  13. import 'widgets/map_filters_sheet.dart';
  14. import 'widgets/map_legend_button.dart';
  15. import 'widgets/map_recenter_button.dart';
  16. import 'widgets/place_details_sheet.dart';
  17.  
  18. class MapScreen extends StatefulWidget {
  19. const MapScreen({super.key});
  20.  
  21. @override
  22. State<MapScreen> createState() => _MapScreenState();
  23. }
  24.  
  25. class _MapScreenState extends State<MapScreen> with WidgetsBindingObserver {
  26. GoogleMapController? _mapController;
  27.  
  28. String? _darkMapStyle;
  29. String? _lightMapStyle;
  30.  
  31. BitmapDescriptor? _busStopMarker;
  32. BitmapDescriptor? _ticketOfficeMarker;
  33. BitmapDescriptor? _attractionMarker;
  34. BitmapDescriptor? _cafeMarker;
  35.  
  36. bool _didLoadAssets = false;
  37. bool? _lastAppliedIsDarkMode;
  38. int _mapRebuildKey = 0;
  39.  
  40. static const CameraPosition _initialCameraPosition = CameraPosition(
  41. target: LatLng(41.3451, 21.5550),
  42. zoom: 13.8,
  43. );
  44.  
  45. @override
  46. void initState() {
  47. super.initState();
  48. WidgetsBinding.instance.addObserver(this);
  49. _loadMapStyles();
  50. }
  51.  
  52. @override
  53. void dispose() {
  54. WidgetsBinding.instance.removeObserver(this);
  55. _mapController?.dispose();
  56. super.dispose();
  57. }
  58.  
  59. @override
  60. void didChangeDependencies() {
  61. super.didChangeDependencies();
  62.  
  63. if (!_didLoadAssets) {
  64. _didLoadAssets = true;
  65. _loadMarkerIcons();
  66. }
  67. }
  68.  
  69. @override
  70. void didChangeAppLifecycleState(AppLifecycleState state) {
  71. if (state == AppLifecycleState.resumed) {
  72. setState(() {
  73. _mapRebuildKey++;
  74. });
  75.  
  76. final isDarkMode = context.read<ThemeProvider>().isDarkMode;
  77.  
  78. WidgetsBinding.instance.addPostFrameCallback((_) async {
  79. await _applyMapStyle(isDarkMode);
  80. });
  81. }
  82. }
  83.  
  84. Future<void> _loadMapStyles() async {
  85. final darkStyle = await rootBundle.loadString(
  86. 'assets/map_styles/dark_map_style.json',
  87. );
  88.  
  89. final lightStyle = await rootBundle.loadString(
  90. 'assets/map_styles/light_map_style.json',
  91. );
  92.  
  93. if (!mounted) return;
  94.  
  95. setState(() {
  96. _darkMapStyle = darkStyle;
  97. _lightMapStyle = lightStyle;
  98. });
  99. }
  100.  
  101. Future<void> _loadMarkerIcons() async {
  102. final imageConfiguration = createLocalImageConfiguration(context);
  103.  
  104. final busStop = await BitmapDescriptor.asset(
  105. imageConfiguration,
  106. 'assets/icons/monkey_stop.png',
  107. width: 28,
  108. height: 28,
  109. );
  110.  
  111. final ticketOffice = await BitmapDescriptor.asset(
  112. imageConfiguration,
  113. 'assets/icons/ticket_office.png',
  114. width: 24,
  115. height: 24,
  116. );
  117.  
  118. final attraction = await BitmapDescriptor.asset(
  119. imageConfiguration,
  120. 'assets/icons/attraction.png',
  121. width: 24,
  122. height: 24,
  123. );
  124.  
  125. final cafe = await BitmapDescriptor.asset(
  126. imageConfiguration,
  127. 'assets/icons/cafe.png',
  128. width: 24,
  129. height: 24,
  130. );
  131.  
  132. if (!mounted) return;
  133.  
  134. setState(() {
  135. _busStopMarker = busStop;
  136. _ticketOfficeMarker = ticketOffice;
  137. _attractionMarker = attraction;
  138. _cafeMarker = cafe;
  139. });
  140. }
  141.  
  142. Future<void> _applyMapStyle(bool isDarkMode) async {
  143. if (_mapController == null) return;
  144.  
  145. final styleToApply = isDarkMode ? _darkMapStyle : _lightMapStyle;
  146. if (styleToApply == null) return;
  147.  
  148. await _mapController!.setMapStyle(styleToApply);
  149. _lastAppliedIsDarkMode = isDarkMode;
  150. }
  151.  
  152. Future<void> _recenterToUser(LocationProvider locationProvider) async {
  153. await locationProvider.refreshLocationState();
  154.  
  155. switch (locationProvider.permissionState) {
  156. case AppLocationPermissionState.denied:
  157. await locationProvider.requestPermission();
  158. await locationProvider.refreshLocationState();
  159. break;
  160.  
  161. case AppLocationPermissionState.deniedForever:
  162. await locationProvider.openAppSettingsPage();
  163. await locationProvider.refreshLocationState();
  164. return;
  165.  
  166. case AppLocationPermissionState.serviceDisabled:
  167. await locationProvider.openLocationSettingsPage();
  168. await locationProvider.refreshLocationState();
  169. if (locationProvider.permissionState ==
  170. AppLocationPermissionState.serviceDisabled) {
  171. return;
  172. }
  173. break;
  174.  
  175. case AppLocationPermissionState.loading:
  176. return;
  177.  
  178. case AppLocationPermissionState.granted:
  179. break;
  180. }
  181.  
  182. if (!locationProvider.isGranted) {
  183. return;
  184. }
  185.  
  186. if (locationProvider.currentPosition == null) {
  187. await locationProvider.getCurrentLocation();
  188. }
  189.  
  190. final position = locationProvider.currentPosition;
  191. if (position == null || _mapController == null) return;
  192.  
  193. await _mapController!.animateCamera(
  194. CameraUpdate.newCameraPosition(
  195. CameraPosition(
  196. target: LatLng(position.latitude, position.longitude),
  197. zoom: 15.5,
  198. ),
  199. ),
  200. );
  201. }
  202.  
  203. void _showLegendDialog() {
  204. showDialog(
  205. context: context,
  206. builder: (context) {
  207. return const AlertDialog(
  208. title: Text('Map Legend'),
  209. content: Column(
  210. mainAxisSize: MainAxisSize.min,
  211. crossAxisAlignment: CrossAxisAlignment.start,
  212. children: [
  213. _LegendImageRow(
  214. imagePath: 'assets/icons/monkey_stop.png',
  215. label: 'Bus stop',
  216. ),
  217. SizedBox(height: 12),
  218. _LegendImageRow(
  219. imagePath: 'assets/icons/ticket_office.png',
  220. label: 'Ticket office',
  221. ),
  222. SizedBox(height: 12),
  223. _LegendImageRow(
  224. imagePath: 'assets/icons/attraction.png',
  225. label: 'Attraction',
  226. ),
  227. SizedBox(height: 12),
  228. _LegendImageRow(
  229. imagePath: 'assets/icons/cafe.png',
  230. label: 'Cafe',
  231. ),
  232. ],
  233. ),
  234. );
  235. },
  236. );
  237. }
  238.  
  239. void _showFiltersSheet() {
  240. showModalBottomSheet(
  241. context: context,
  242. showDragHandle: false,
  243. isScrollControlled: true,
  244. builder: (_) => const MapFiltersSheet(),
  245. );
  246. }
  247.  
  248. void _showPlaceDetails(LocationModel location) {
  249. showModalBottomSheet(
  250. context: context,
  251. isScrollControlled: true,
  252. showDragHandle: false,
  253. builder: (_) => PlaceDetailsSheet(location: location),
  254. );
  255. }
  256.  
  257. Color _hexToColor(String hex) {
  258. final cleanedHex = hex.replaceAll('#', '');
  259. return Color(int.parse('FF$cleanedHex', radix: 16));
  260. }
  261.  
  262. Set<Polyline> _buildPolylines(MapProvider mapProvider) {
  263. return mapProvider.selectedRoutes.map((route) {
  264. final line = mapProvider.busLines.firstWhere(
  265. (line) => line.id == route.lineId,
  266. );
  267.  
  268. return Polyline(
  269. polylineId: PolylineId(route.id),
  270. width: 5,
  271. color: _hexToColor(line.colorHex),
  272. points: route.polylinePoints
  273. .map((point) {
  274. final lat = point['latitude'];
  275. final lng = point['longitude'];
  276.  
  277. if (lat is num && lng is num) {
  278. final latDouble = lat.toDouble();
  279. final lngDouble = lng.toDouble();
  280.  
  281. if (latDouble >= 41.0 &&
  282. latDouble <= 41.5 &&
  283. lngDouble >= 21.2 &&
  284. lngDouble <= 21.8) {
  285. return LatLng(latDouble, lngDouble);
  286. }
  287. }
  288.  
  289. return null;
  290. })
  291. .whereType<LatLng>()
  292. .toList(),
  293. );
  294. }).toSet();
  295. }
  296.  
  297. BitmapDescriptor _markerIconForType(String type) {
  298. switch (type) {
  299. case 'bus_stop':
  300. return _busStopMarker ?? BitmapDescriptor.defaultMarker;
  301. case 'ticket_office':
  302. return _ticketOfficeMarker ?? BitmapDescriptor.defaultMarker;
  303. case 'landmark':
  304. return _attractionMarker ?? BitmapDescriptor.defaultMarker;
  305. case 'cafe':
  306. return _cafeMarker ?? BitmapDescriptor.defaultMarker;
  307. default:
  308. return BitmapDescriptor.defaultMarker;
  309. }
  310. }
  311.  
  312. Set<Marker> _buildMarkers(MapProvider mapProvider) {
  313. final visibleBusStops = mapProvider.selectedRouteStops;
  314. final otherLocations = mapProvider.locations
  315. .where((location) => location.type != 'bus_stop')
  316. .toList();
  317.  
  318. final allVisibleLocations = [
  319. ...visibleBusStops,
  320. ...otherLocations,
  321. ];
  322.  
  323. return allVisibleLocations.map((location) {
  324. final lineNumbers = mapProvider.getLineNumbersForStop(location.id);
  325.  
  326. return Marker(
  327. markerId: MarkerId(location.id),
  328. position: LatLng(location.latitude, location.longitude),
  329. icon: _markerIconForType(location.type),
  330. infoWindow: InfoWindow(
  331. title: location.name,
  332. snippet: location.type == 'bus_stop'
  333. ? 'Lines: ${lineNumbers.join(', ')}'
  334. : mapProvider.getShortDescription(location.description),
  335. ),
  336. onTap: () {
  337. if (location.type == 'landmark' || location.type == 'cafe') {
  338. _showPlaceDetails(location);
  339. }
  340. },
  341. );
  342. }).toSet();
  343. }
  344.  
  345. @override
  346. Widget build(BuildContext context) {
  347. final locationProvider = context.watch<LocationProvider>();
  348. final mapProvider = context.watch<MapProvider>();
  349. final isDarkMode = context.watch<ThemeProvider>().isDarkMode;
  350.  
  351. if (_mapController != null && _lastAppliedIsDarkMode != isDarkMode) {
  352. WidgetsBinding.instance.addPostFrameCallback((_) {
  353. _applyMapStyle(isDarkMode);
  354. });
  355. }
  356.  
  357. if (mapProvider.isLoading && !mapProvider.hasData) {
  358. return const Center(
  359. child: CircularProgressIndicator(),
  360. );
  361. }
  362.  
  363. if (mapProvider.errorMessage != null && !mapProvider.hasData) {
  364. return AppErrorState(
  365. message: mapProvider.errorMessage!,
  366. imageAssetPath: AppErrorMessages.imageForType(mapProvider.errorType),
  367. onRetry: mapProvider.loadMapData,
  368. );
  369. }
  370.  
  371. return Stack(
  372. children: [
  373. GoogleMap(
  374. key: ValueKey(_mapRebuildKey),
  375. initialCameraPosition: _initialCameraPosition,
  376. myLocationEnabled: locationProvider.isGranted,
  377. myLocationButtonEnabled: false,
  378. zoomControlsEnabled: false,
  379. compassEnabled: true,
  380. mapToolbarEnabled: false,
  381. polylines: _buildPolylines(mapProvider),
  382. markers: _buildMarkers(mapProvider),
  383. onMapCreated: (controller) async {
  384. _mapController = controller;
  385. await _applyMapStyle(isDarkMode);
  386. },
  387. ),
  388. Positioned(
  389. right: 16,
  390. top: MediaQuery.of(context).size.height * 0.34,
  391. child: Column(
  392. children: [
  393. MapRecenterButton(
  394. onPressed: () => _recenterToUser(locationProvider),
  395. ),
  396. const SizedBox(height: 12),
  397. MapLegendButton(
  398. onPressed: _showLegendDialog,
  399. ),
  400. ],
  401. ),
  402. ),
  403. Positioned(
  404. left: 0,
  405. right: 0,
  406. bottom: 36,
  407. child: MapFiltersButton(
  408. onPressed: _showFiltersSheet,
  409. ),
  410. ),
  411. ],
  412. );
  413. }
  414. }
  415.  
  416. class _LegendImageRow extends StatelessWidget {
  417. final String imagePath;
  418. final String label;
  419.  
  420. const _LegendImageRow({
  421. required this.imagePath,
  422. required this.label,
  423. });
  424.  
  425. @override
  426. Widget build(BuildContext context) {
  427. return Row(
  428. children: [
  429. Image.asset(
  430. imagePath,
  431. width: 22,
  432. height: 22,
  433. ),
  434. const SizedBox(width: 10),
  435. Text(label),
  436. ],
  437. );
  438. }
  439. }
Advertisement
Add Comment
Please, Sign In to add comment