dzocesrce

Untitled

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