import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:geolocator/geolocator.dart'; import 'dart:async'; import '../models/train_record.dart'; import 'package:lbjconsole/services/database_service.dart'; import 'package:latlong2/latlong.dart'; class MapWebViewScreen extends StatefulWidget { const MapWebViewScreen({super.key}); @override State createState() => MapWebViewScreenState(); } class MapWebViewScreenState extends State with WidgetsBindingObserver { late final WebViewController _controller; Position? _currentPosition; List _trainRecords = []; bool _isRailwayLayerVisible = true; bool _isLoading = true; String _timeFilter = 'unlimited'; Timer? _refreshTimer; Timer? _locationUpdateTimer; bool _isMapInitialized = false; bool _isLocationPermissionGranted = false; double _currentZoom = 14.0; double _currentRotation = 0.0; LatLng? _currentLocation; LatLng? _lastTrainLocation; bool _isDataLoaded = false; final Completer _webViewReadyCompleter = Completer(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initializeWebView(); _startInitialization(); } Future _startInitialization() async { setState(() { _isLoading = true; }); try { await _loadSettings(); await _loadTrainRecordsFromDatabase(); await _webViewReadyCompleter.future; _initializeMapCamera(); _initializeLocation(); _startAutoRefresh(); } catch (e, s) { print('[Flutter] Init Map WebView Screen Failed: $e\n$s'); } finally { if (mounted) { setState(() { _isLoading = false; }); } } } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _reloadSettingsIfNeeded(); } } Future _initializeWebView() async { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0xFF121212)) ..addJavaScriptChannel( 'showTrainDetails', onMessageReceived: (JavaScriptMessage message) { try { final trainData = jsonDecode(message.message); _showTrainDetailsDialog(trainData); } catch (e) {} }, ) ..addJavaScriptChannel( 'onMapStateChanged', onMessageReceived: (JavaScriptMessage message) { try { final mapState = jsonDecode(message.message); _onMapStateChanged(mapState); } catch (e) {} }, ) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) {}, onPageStarted: (String url) { setState(() { _isLoading = true; }); }, onPageFinished: (String url) { setState(() { _isLoading = false; }); if (!_webViewReadyCompleter.isCompleted) { _webViewReadyCompleter.complete(); } }, onWebResourceError: (WebResourceError error) { setState(() { _isLoading = false; }); }, ), ) ..loadFlutterAsset('assets/mapbox_map.html') .then((_) {}) .catchError((error) {}); await Future.delayed(const Duration(milliseconds: 500)); } Future _initializeLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请开启定位服务')), ); } return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.whileInUse || permission == LocationPermission.always) { Position position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.best, timeLimit: Duration(seconds: 15), ), ); setState(() { _currentPosition = position; _isLocationPermissionGranted = true; }); _updateUserLocation(); _startLocationUpdates(); } else if (permission == LocationPermission.deniedForever) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')), ); } } } catch (e) {} } void _startLocationUpdates() { _locationUpdateTimer = Timer.periodic(const Duration(seconds: 30), (timer) { if (_isLocationPermissionGranted && mounted) { _updateCurrentLocation(); } else {} }); } Future _updateCurrentLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { return; } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.best, forceAndroidLocationManager: true, ); setState(() { _currentPosition = position; }); _updateUserLocation(); } catch (e) { if (e.toString().contains('PERMISSION_DENIED') || e.toString().contains('permission')) { _checkAndRequestPermissions(); } } } Future _checkAndRequestPermissions() async { try { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.whileInUse || permission == LocationPermission.always) { setState(() { _isLocationPermissionGranted = true; }); } else { setState(() { _isLocationPermissionGranted = false; }); } } catch (e) {} } Future _loadTrainRecordsFromDatabase() async { try { final isConnected = await DatabaseService.instance.isDatabaseConnected(); if (!isConnected) {} List records; if (_timeFilter == 'unlimited') { records = await DatabaseService.instance.getAllRecords(); } else { final duration = _getTimeFilterDuration(_timeFilter); if (duration != null && duration != Duration.zero) { records = await DatabaseService.instance .getRecordsWithinReceivedTimeRange(duration); } else { records = await DatabaseService.instance.getAllRecords(); } } if (records.isNotEmpty) { for (int i = 0; i < math.min(3, records.length); i++) { final record = records[i]; final coords = record.getCoordinates(); } } setState(() { _trainRecords = records; _isLoading = false; if (_trainRecords.isNotEmpty) { final validRecords = [ ..._getValidRecords(), ..._getValidDmsRecords() ]; if (validRecords.isNotEmpty) { final lastRecord = validRecords.first; LatLng? position; final dmsPosition = _parseDmsCoordinate(lastRecord.positionInfo); if (dmsPosition != null) { position = dmsPosition; } else { final coords = lastRecord.getCoordinates(); if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { position = LatLng(coords['lat']!, coords['lng']!); } } if (position != null) { _lastTrainLocation = position; } else {} } else {} } else {} }); Future.delayed(const Duration(milliseconds: 3000), () { if (mounted) { _updateTrainMarkers(); } }); Future.delayed(const Duration(milliseconds: 5000), () { if (mounted) { _updateTrainMarkers(); } }); } catch (e, stackTrace) { setState(() { _isLoading = false; }); } } Duration? _getTimeFilterDuration(String filter) { switch (filter) { case '1hour': return const Duration(hours: 1); case '6hours': return const Duration(hours: 6); case '12hours': return const Duration(hours: 12); case '24hours': return const Duration(hours: 24); case '7days': return const Duration(days: 7); case '30days': return const Duration(days: 30); default: return null; } } void _startAutoRefresh() { _refreshTimer = Timer.periodic(const Duration(seconds: 10), (timer) { if (mounted) { _loadTrainRecordsFromDatabase(); _reloadSettingsIfNeeded(); } }); } void _reloadSettingsIfNeeded() async { try { final settings = await DatabaseService.instance.getAllSettings(); if (settings != null) { final newTimeFilter = settings['mapTimeFilter'] as String? ?? 'unlimited'; if (newTimeFilter != _timeFilter) { if (mounted) { setState(() { _timeFilter = newTimeFilter; }); } _loadTrainRecordsFromDatabase(); } } } catch (e) {} } @override void didChangeDependencies() { super.didChangeDependencies(); _reloadSettingsIfNeeded(); } void _loadTrainRecords() async { setState(() => _isLoading = true); try { await _loadTrainRecordsFromDatabase(); } catch (e) { setState(() => _isLoading = false); } } void _initializeMapCamera() { if (_isMapInitialized) return; LatLng targetLocation; double targetZoom = _currentZoom; double targetRotation = _currentRotation; if (_currentLocation != null) { targetLocation = _currentLocation!; } else if (_lastTrainLocation != null) { targetLocation = _lastTrainLocation!; targetZoom = 14.0; targetRotation = 0.0; } else { targetLocation = const LatLng(39.9042, 116.4074); targetZoom = 10.0; } _centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation); _isMapInitialized = true; Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { _updateUserLocation(); _updateTrainMarkers(); _controller.runJavaScript(''' if (window.MapInterface) { window.MapInterface.setRailwayVisible($_isRailwayLayerVisible); } '''); } }); } void _centerMap(LatLng location, {double? zoom, double? rotation}) { final targetZoom = zoom ?? _currentZoom; final targetRotation = rotation ?? _currentRotation; _controller.runJavaScript(''' (function() { if (window.MapInterface) { try { window.MapInterface.setCenter(${location.latitude}, ${location.longitude}, $targetZoom, $targetRotation); } catch (error) { } } })(); ''').then((result) {}).catchError((error) {}); } Future _loadSettings() async { try { final settings = await DatabaseService.instance.getAllSettings(); if (settings != null) { setState(() { _isRailwayLayerVisible = (settings['mapRailwayLayerVisible'] as int?) == 1; _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0; _currentRotation = (settings['mapRotation'] as num?)?.toDouble() ?? 0.0; _timeFilter = settings['mapTimeFilter'] as String? ?? 'unlimited'; final lat = (settings['mapCenterLat'] as num?)?.toDouble(); final lon = (settings['mapCenterLon'] as num?)?.toDouble(); if (lat != null && lon != null && lat != 0.0 && lon != 0.0) { _currentLocation = LatLng(lat, lon); } }); } } catch (e) {} } Future _saveSettings() async { try { final settings = { 'mapRailwayLayerVisible': _isRailwayLayerVisible ? 1 : 0, 'mapZoomLevel': _currentZoom, 'mapRotation': _currentRotation, 'mapTimeFilter': _timeFilter, 'mapSettingsTimestamp': DateTime.now().millisecondsSinceEpoch, }; if (_currentLocation != null) { settings['mapCenterLat'] = _currentLocation!.latitude; settings['mapCenterLon'] = _currentLocation!.longitude; } await DatabaseService.instance.updateSettings(settings); } catch (e) {} } LatLng? _parseDmsCoordinate(String? positionInfo) { if (positionInfo == null || positionInfo.isEmpty || positionInfo == '') { return null; } try { final parts = positionInfo.trim().split(' '); if (parts.length >= 2) { final latStr = parts[0]; final lngStr = parts[1]; final lat = _parseDmsString(latStr); final lng = _parseDmsString(lngStr); if (lat != null && lng != null && (lat.abs() > 0.001 || lng.abs() > 0.001)) { return LatLng(lat, lng); } } } catch (e) {} return null; } double? _parseDmsString(String dmsStr) { try { final degreeIndex = dmsStr.indexOf('°'); if (degreeIndex == -1) return null; final degrees = double.tryParse(dmsStr.substring(0, degreeIndex)); if (degrees == null) return null; final minuteIndex = dmsStr.indexOf('′'); if (minuteIndex == -1) return degrees; final minutes = double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex)); if (minutes == null) return degrees; return degrees + (minutes / 60.0); } catch (e) { return null; } } List _getValidRecords() { return _trainRecords.where((record) { final coords = record.getCoordinates(); return coords['lat'] != 0.0 && coords['lng'] != 0.0; }).toList(); } List _getValidDmsRecords() { return _trainRecords.where((record) { return _parseDmsCoordinate(record.positionInfo) != null; }).toList(); } void _updateTrainMarkers() { if (_trainRecords.isEmpty) { return; } if (!_isMapInitialized) { Future.delayed(const Duration(seconds: 2), () { if (mounted && _trainRecords.isNotEmpty) { _updateTrainMarkers(); } }); return; } final validRecords = [..._getValidRecords(), ..._getValidDmsRecords()]; final trainData = validRecords .map((record) { LatLng? position; final dmsPosition = _parseDmsCoordinate(record.positionInfo); if (dmsPosition != null) { position = dmsPosition; } else { final coords = record.getCoordinates(); if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { position = LatLng(coords['lat']!, coords['lng']!); } } if (position != null) { return { 'lat': position.latitude, 'lng': position.longitude, 'fullTrainNumber': record.fullTrainNumber, 'time': record.time, 'speed': record.speed, 'position': record.position, 'route': record.route, 'locoType': record.locoType, 'loco': record.loco, 'timestamp': record.timestamp, }; } return null; }) .where((data) => data != null) .toList(); trainData.sort((a, b) { final aTimestamp = a?['timestamp']; final bTimestamp = b?['timestamp']; if (aTimestamp == null || bTimestamp == null) return 0; if (aTimestamp is DateTime && bTimestamp is DateTime) { return bTimestamp.compareTo(aTimestamp); } if (aTimestamp is int && bTimestamp is int) { return bTimestamp.compareTo(aTimestamp); } if (aTimestamp is String && bTimestamp is String) { return bTimestamp.compareTo(aTimestamp); } return 0; }); final jsonTrainData = trainData .map((train) { if (train == null) return null; final trainCopy = Map.from(train); if (trainCopy['timestamp'] is DateTime) { trainCopy['timestamp'] = (trainCopy['timestamp'] as DateTime).millisecondsSinceEpoch; } return trainCopy; }) .where((train) => train != null) .toList(); if (trainData.isEmpty) { return; } if (trainData.isNotEmpty) { final latestTrain = trainData.first; if (latestTrain != null) { _lastTrainLocation = LatLng((latestTrain['lat'] as num).toDouble(), (latestTrain['lng'] as num).toDouble()); } } _controller.runJavaScript(''' (function() { try { if (window.MapInterface && window.MapInterface.updateTrainMarkers) { window.MapInterface.updateTrainMarkers(${jsonEncode(jsonTrainData)}); if (!window.trainMarkersUpdated && ${trainData.length} > 0) { window.trainMarkersUpdated = true; setTimeout(function() { if (window.MapInterface && window.MapInterface.fitBounds) { window.MapInterface.fitBounds(); } }, 1000); } } else { console.error('MapInterface 未定义或updateTrainMarkers方法不存在'); } } catch (error) { console.error('更新列车标记失败:', error); } })(); ''').then((result) {}).catchError((error) {}); } void _updateUserLocation() { if (_currentPosition != null) { _controller.runJavaScript(''' (function() { try { if (window.MapInterface && window.MapInterface.setUserLocation) { window.MapInterface.setUserLocation(${_currentPosition!.latitude}, ${_currentPosition!.longitude}); if (!window.userLocationUpdated) { window.userLocationUpdated = true; } } else { console.error('MapInterface 未定义或setUserLocation方法不存在'); } } catch (error) { console.error('更新用户位置失败:', error); } })(); ''').then((_) {}).catchError((error) {}); } else {} } void _toggleRailwayLayer() { setState(() { _isRailwayLayerVisible = !_isRailwayLayerVisible; }); _controller.runJavaScript(''' (function() { if (window.MapInterface) { try { window.MapInterface.setRailwayVisible($_isRailwayLayerVisible); console.log('铁路图层切换成功: $_isRailwayLayerVisible'); } catch (error) { console.error('切换铁路图层失败:', error); } } else { console.error('MapInterface 未定义'); } })(); ''').then((result) {}).catchError((error) {}); } void _showTrainDetailsDialog(Map train) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: const Color(0xFF1E1E1E), title: Text( train['trainNumber'] ?? '未知车次', style: const TextStyle(color: Colors.white), ), content: SingleChildScrollView( child: ListBody( children: [ _buildDetailRow('车次', train['trainNumber'] ?? '未知'), _buildDetailRow('类型', train['trainType'] ?? '未知'), _buildDetailRow('速度', '${train['speed'] ?? 0} km/h'), _buildDetailRow('方向', '${train['direction'] ?? 0}°'), _buildDetailRow( '经度', train['longitude']?.toStringAsFixed(6) ?? '未知'), _buildDetailRow( '纬度', train['latitude']?.toStringAsFixed(6) ?? '未知'), _buildDetailRow('时间', _getDisplayTime(train['timestamp'])), _buildDetailRow('日期', _getDisplayDate(train['timestamp'])), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('关闭', style: TextStyle(color: Colors.white)), ), TextButton( onPressed: () { Navigator.of(context).pop(); _controller.runJavaScript(''' if (window.MapInterface) { window.MapInterface.setCenter(${train['latitude']}, ${train['longitude']}, 16); } '''); }, child: const Text('定位', style: TextStyle(color: Color(0xFF007ACC))), ), ], ); }, ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 80, child: Text( '$label:', style: const TextStyle( color: Colors.grey, fontSize: 14, ), ), ), Expanded( child: Text( value, style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ), ], ), ); } String _getDisplayTime(int? timestamp) { if (timestamp == null) return '未知'; final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}'; } String _getDisplayDate(int? timestamp) { if (timestamp == null) return '未知'; final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}'; } void _showTimeFilterDialog() { showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: const Text('时间筛选'), children: [ SimpleDialogOption( onPressed: () { setState(() { _timeFilter = 'unlimited'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('无限制'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '1hour'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近1小时'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '6hours'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近6小时'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '12hours'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近12小时'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '24hours'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近24小时'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '7days'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近7天'), ), SimpleDialogOption( onPressed: () { setState(() { _timeFilter = '30days'; }); Navigator.of(context).pop(); _saveSettingsAndReload(); }, child: const Text('最近30天'), ), ], ); }, ); } void _saveSettingsAndReload() async { try { await _saveSettings(); _loadTrainRecordsFromDatabase(); } catch (e) {} } String _getTimeFilterLabel() { switch (_timeFilter) { case '1hour': return '1小时'; case '6hours': return '6小时'; case '12hours': return '12小时'; case '24hours': return '24小时'; case '7days': return '7天'; case '30days': return '30天'; default: return '无限制'; } } void _onMapStateChanged(Map mapState) { try { final lat = (mapState['lat'] as num).toDouble(); final lng = (mapState['lng'] as num).toDouble(); final zoom = (mapState['zoom'] as num).toDouble(); final bearing = (mapState['bearing'] as num).toDouble(); setState(() { _currentLocation = LatLng(lat, lng); _currentZoom = zoom; _currentRotation = bearing; }); Future.microtask(() { if (mounted) { _saveSettings(); } }); } catch (e) {} } void _centerToUserLocation() async { if (_currentPosition != null) { final userLocation = LatLng(_currentPosition!.latitude, _currentPosition!.longitude); _centerMap(userLocation, zoom: 15.0); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('正在获取当前位置...')), ); } try { await _forceUpdateLocation(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('获取位置失败: $e')), ); } } } } void _centerToLastTrain() { if (_trainRecords.isNotEmpty && _lastTrainLocation != null) { _centerMap(_lastTrainLocation!, zoom: 15.0); } } Future _forceUpdateLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请开启定位服务')), ); } return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')), ); } return; } if (permission != LocationPermission.whileInUse && permission != LocationPermission.always) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('定位权限不足')), ); } return; } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.best, forceAndroidLocationManager: true, timeLimit: const Duration(seconds: 20), ); final newLocation = LatLng(position.latitude, position.longitude); setState(() { _currentPosition = position; _isLocationPermissionGranted = true; }); _centerMap(newLocation, zoom: 16.0); _updateUserLocation(); } catch (e) {} } void _refreshMap() { _loadTrainRecordsFromDatabase(); if (_isLocationPermissionGranted) { _forceUpdateLocation(); } else { _updateUserLocation(); } _controller.runJavaScript(''' if (window.MapInterface) { window.MapInterface.getTrainMarkersCount(); } '''); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _saveSettings(); _refreshTimer?.cancel(); _locationUpdateTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF121212), body: Stack( children: [ WebViewWidget(controller: _controller), if (_isLoading) Container( color: const Color(0xFF121212).withValues(alpha: 0.8), child: const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)), ), ), ), Positioned( right: 16, bottom: 80, child: Column( mainAxisSize: MainAxisSize.min, children: [ FloatingActionButton( heroTag: "time_filter", mini: true, backgroundColor: const Color(0xFF1E1E1E), onPressed: () { _reloadSettingsIfNeeded(); _showTimeFilterDialog(); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.filter_list, color: Colors.white, size: 18), Text( _getTimeFilterLabel(), style: const TextStyle(color: Colors.white, fontSize: 8), ), ], ), ), const SizedBox(height: 8), FloatingActionButton( heroTag: "refresh", mini: true, backgroundColor: const Color(0xFF1E1E1E), onPressed: _refreshMap, child: const Icon(Icons.refresh, color: Colors.white), ), const SizedBox(height: 8), FloatingActionButton( heroTag: "layers", mini: true, backgroundColor: const Color(0xFF1E1E1E), onPressed: _toggleRailwayLayer, child: Icon( _isRailwayLayerVisible ? Icons.layers : Icons.layers_outlined, color: Colors.white, ), ), const SizedBox(height: 8), FloatingActionButton( heroTag: "location", mini: true, backgroundColor: const Color(0xFF1E1E1E), onPressed: _centerToUserLocation, child: const Icon(Icons.my_location, color: Colors.white), ), const SizedBox(height: 8), FloatingActionButton( heroTag: "last_train", mini: true, backgroundColor: const Color(0xFF1E1E1E), onPressed: _centerToLastTrain, child: const Icon(Icons.train, color: Colors.white), ), ], ), ), ], ), ); } }