import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import 'package:lbjconsole/services/database_service.dart'; import 'package:lbjconsole/models/train_record.dart'; class MapScreen extends StatefulWidget { const MapScreen({super.key}); @override State createState() => _MapScreenState(); } class _MapScreenState extends State { final MapController _mapController = MapController(); final List _trainRecords = []; bool _isLoading = true; bool _railwayLayerVisible = true; LatLng? _currentLocation; LatLng? _lastTrainLocation; LatLng? _userLocation; double _currentZoom = 12.0; double _currentRotation = 0.0; bool _isMapInitialized = false; bool _isFollowingLocation = false; bool _isLocationPermissionGranted = false; Timer? _locationTimer; @override void initState() { super.initState(); _initializeMap(); _loadTrainRecords(); _loadSettings(); _startLocationUpdates(); } @override void dispose() { _saveSettings(); _locationTimer?.cancel(); super.dispose(); } Future _initializeMap() async {} Future _requestLocationPermission() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { 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) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')), ); return; } setState(() { _isLocationPermissionGranted = true; }); _getCurrentLocation(); } Future _getCurrentLocation() async { try { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, forceAndroidLocationManager: true, ); setState(() { _userLocation = LatLng(position.latitude, position.longitude); }); } catch (e) {} } void _startLocationUpdates() { _requestLocationPermission(); _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) { if (_isLocationPermissionGranted) { _getCurrentLocation(); } }); } Future _forceUpdateLocation() async { try { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.best, forceAndroidLocationManager: true, ); final newLocation = LatLng(position.latitude, position.longitude); setState(() { _userLocation = newLocation; }); _mapController.move(newLocation, 15.0); } catch (e) { print('强制更新位置失败: $e'); } } Future _loadSettings() async { try { final settings = await DatabaseService.instance.getAllSettings(); if (settings != null) { setState(() { _railwayLayerVisible = (settings['mapRailwayLayerVisible'] as int?) == 1; _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0; _currentRotation = (settings['mapRotation'] as num?)?.toDouble() ?? 0.0; final lat = (settings['mapCenterLat'] as num?)?.toDouble(); final lon = (settings['mapCenterLon'] as num?)?.toDouble(); if (lat != null && lon != null) { _currentLocation = LatLng(lat, lon); } }); } } catch (e) {} } Future _saveSettings() async { try { final center = _mapController.camera.center; await DatabaseService.instance.updateSettings({ 'mapRailwayLayerVisible': _railwayLayerVisible ? 1 : 0, 'mapZoomLevel': _currentZoom, 'mapCenterLat': center.latitude, 'mapCenterLon': center.longitude, 'mapRotation': _currentRotation, }); } catch (e) {} } Future _loadTrainRecords() async { setState(() => _isLoading = true); try { final records = await DatabaseService.instance.getAllRecords(); setState(() { _trainRecords.clear(); _trainRecords.addAll(records); _isLoading = false; if (_trainRecords.isNotEmpty) { final lastRecord = _trainRecords.first; final coords = lastRecord.getCoordinates(); final dmsCoords = _parseDmsCoordinate(lastRecord.positionInfo); if (dmsCoords != null) { _lastTrainLocation = dmsCoords; } else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { _lastTrainLocation = LatLng(coords['lat']!, coords['lng']!); } } _initializeMapPosition(); }); } catch (e) { setState(() => _isLoading = false); } } void _initializeMapPosition() { if (_isMapInitialized) return; LatLng? targetLocation; if (_currentLocation != null) { targetLocation = _currentLocation; } else if (_userLocation != null) { targetLocation = _userLocation; } else if (_lastTrainLocation != null) { targetLocation = _lastTrainLocation; } else { _isMapInitialized = true; return; } _centerMap(targetLocation!, zoom: _currentZoom); _isMapInitialized = true; } void _centerMap(LatLng location, {double? zoom}) { _mapController.move(location, zoom ?? _currentZoom); } 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) { print('解析DMS坐标失败: $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(); } List _buildTrainMarkers() { final markers = []; final validRecords = [..._getValidRecords(), ..._getValidDmsRecords()]; for (final record in validRecords) { 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) { final trainDisplay = record.fullTrainNumber.isEmpty ? "未知列车" : record.fullTrainNumber; markers.add( Marker( point: position, width: 80, height: 60, child: GestureDetector( onTap: () => position != null ? _showTrainDetailsDialog(record, position) : null, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white, width: 2), ), child: const Icon( Icons.train, color: Colors.white, size: 18, ), ), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(2), ), child: Text( trainDisplay, style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ], ), ), ), ); } } return markers; } void _centerToMyLocation() { _centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), zoom: 15.0); } void _centerToLastTrain() { if (_trainRecords.isNotEmpty) { final lastRecord = _trainRecords.first; final coords = lastRecord.getCoordinates(); final dmsCoords = _parseDmsCoordinate(lastRecord.positionInfo); LatLng? targetPosition; if (dmsCoords != null) { targetPosition = dmsCoords; } else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { targetPosition = LatLng(coords['lat']!, coords['lng']!); } if (targetPosition != null) { _centerMap(targetPosition, zoom: 15.0); } } } void _showTrainDetailsDialog(TrainRecord record, LatLng position) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), builder: (context) { return Container( width: double.infinity, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), ), child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 4, height: 24, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 12), Expanded( child: Text( record.fullTrainNumber.isEmpty ? "未知列车" : record.fullTrainNumber, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 16), Container( width: double.infinity, decoration: BoxDecoration( color: Theme.of(context) .colorScheme .surfaceVariant .withOpacity(0.3), borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _buildMaterial3DetailRow( context, "时间", record.formattedTime), _buildMaterial3DetailRow( context, "日期", record.formattedDate), _buildMaterial3DetailRow( context, "类型", record.trainType), _buildMaterial3DetailRow(context, "速度", "${record.speed.replaceAll(' ', '')} km/h"), _buildMaterial3DetailRow( context, "位置", record.position.trim().endsWith('.') ? '${record.position.trim().substring(0, record.position.trim().length - 1)}K' : '${record.position.trim()}K'), _buildMaterial3DetailRow( context, "路线", record.route.trim().endsWith('.') ? record.route.trim().substring( 0, record.route.trim().length - 1) : record.route.trim()), _buildMaterial3DetailRow( context, "机车", "${record.locoType}-${record.loco}"), _buildMaterial3DetailRow(context, "坐标", "${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}"), ], ), ), ), const SizedBox(height: 24), Row( children: [ Expanded( child: FilledButton.tonal( onPressed: () => Navigator.pop(context), child: const Text('关闭'), ), ), const SizedBox(width: 12), Expanded( child: FilledButton( onPressed: () { Navigator.pop(context); _centerMap(position, zoom: 17.0); }, child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.my_location, size: 16), SizedBox(width: 8), Text('居中查看'), ], ), ), ), ], ), ], ), ), ); }, ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 80, child: Text( label, style: const TextStyle(color: Colors.grey, fontSize: 14), ), ), Expanded( child: Text( value.isEmpty ? "未知" : value, style: const TextStyle(color: Colors.white, fontSize: 14), ), ), ], ), ); } Widget _buildMaterial3DetailRow( BuildContext context, String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 60, child: Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), Expanded( child: Text( value.isEmpty ? "未知" : value, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), ), ], ), ); } @override Widget build(BuildContext context) { final markers = _buildTrainMarkers(); if (_userLocation != null) { markers.add( Marker( point: _userLocation!, width: 40, height: 40, child: Container( decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), child: const Icon( Icons.my_location, color: Colors.white, size: 20, ), ), ), ); } return Scaffold( backgroundColor: const Color(0xFF121212), body: Stack( children: [ FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _lastTrainLocation ?? const LatLng(39.9042, 116.4074), initialZoom: _currentZoom, initialRotation: _currentRotation, minZoom: 4.0, maxZoom: 18.0, onPositionChanged: (MapCamera camera, bool hasGesture) { if (hasGesture) { setState(() { _currentLocation = camera.center; _currentZoom = camera.zoom; _currentRotation = camera.rotation; }); _saveSettings(); } }, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'org.noxylva.lbjconsole', ), if (_railwayLayerVisible) TileLayer( urlTemplate: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], userAgentPackageName: 'org.noxylva.lbjconsole', ), MarkerLayer( markers: markers, ), ], ), if (_isLoading) const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)), ), ), Positioned( right: 16, top: 40, child: Column( children: [ FloatingActionButton.small( heroTag: 'railwayLayer', backgroundColor: const Color(0xFF1E1E1E), onPressed: () { setState(() { _railwayLayerVisible = !_railwayLayerVisible; }); _saveSettings(); }, child: Icon( _railwayLayerVisible ? Icons.layers : Icons.layers_outlined, color: Colors.white, ), ), const SizedBox(height: 8), FloatingActionButton.small( heroTag: 'myLocation', backgroundColor: const Color(0xFF1E1E1E), onPressed: () { _forceUpdateLocation(); }, child: const Icon(Icons.my_location, color: Colors.white), ), const SizedBox(height: 8), ], ), ), ], ), ); } }