Files
LBJ_Console/lib/screens/map_webview_screen.dart
2025-09-29 18:44:15 +08:00

1163 lines
34 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<MapWebViewScreen> createState() => MapWebViewScreenState();
}
class MapWebViewScreenState extends State<MapWebViewScreen>
with WidgetsBindingObserver {
late final WebViewController _controller;
Position? _currentPosition;
List<TrainRecord> _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;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeWebView();
_initializeServices();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_reloadSettingsIfNeeded();
}
}
Future<void> _initializeServices() async {
try {
await _loadSettings();
await _loadTrainRecordsFromDatabase();
_initializeLocation();
_startAutoRefresh();
} catch (e) {}
}
void _initializeWebView() {
_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 (mounted) {
_initializeMap();
_initializeLocation();
}
},
onWebResourceError: (WebResourceError error) {
setState(() {
_isLoading = false;
});
},
),
)
..loadFlutterAsset('assets/mapbox_map.html')
.then((_) {})
.catchError((error) {});
}
Future<void> _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<void> _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<void> _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<void> _loadTrainRecordsFromDatabase() async {
try {
final isConnected = await DatabaseService.instance.isDatabaseConnected();
if (!isConnected) {}
List<TrainRecord> 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 {}
if (mounted) {
_initializeMapPosition();
}
});
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 _initializeMapPosition() {
if (_isMapInitialized) return;
LatLng? targetLocation;
if (_lastTrainLocation != null) {
targetLocation = _lastTrainLocation;
} else if (_currentPosition != null) {
targetLocation =
LatLng(_currentPosition!.latitude, _currentPosition!.longitude);
}
if (targetLocation != null) {
_centerMap(targetLocation,
zoom: _currentZoom, rotation: _currentRotation);
_isMapInitialized = true;
} else {
_isMapInitialized = true;
}
}
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<void> _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<void> _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) {}
}
Future<void> _initializeMap() async {
try {
LatLng? targetLocation;
double targetZoom = 14.0;
double targetRotation = _currentRotation;
if (_lastTrainLocation != null) {
targetLocation = _lastTrainLocation;
targetZoom = 14.0;
targetRotation = 0.0;
} else if (_currentPosition != null) {
targetLocation =
LatLng(_currentPosition!.latitude, _currentPosition!.longitude);
targetZoom = 16.0;
targetRotation = 0.0;
}
if (targetLocation != null) {
_centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation);
try {
await _controller.runJavaScript('''
(function() {
if (window.map && window.map.getContainer) {
window.map.getContainer().style.opacity = '1';
return true;
} else {
return false;
}
})();
''');
} catch (e) {
print('显示地图失败: $e');
Future.delayed(const Duration(milliseconds: 500), () {
_controller.runJavaScript('''
(function() {
if (window.map && window.map.getContainer) {
window.map.getContainer().style.opacity = '1';
console.log('延迟显示地图成功');
}
})();
''');
});
}
}
setState(() {
_isMapInitialized = true;
});
} catch (e, stackTrace) {}
Future.delayed(const Duration(milliseconds: 1000), () {
if (mounted && _isMapInitialized) {
_updateTrainMarkers();
_controller.runJavaScript('''
(function() {
if (window.MapInterface) {
try {
window.MapInterface.setRailwayVisible($_isRailwayLayerVisible);
console.log('初始化铁路图层状态: $_isRailwayLayerVisible');
} catch (error) {
console.error('初始化铁路图层失败:', error);
}
}
})();
''');
}
});
Future.delayed(const Duration(milliseconds: 3000), () {
if (mounted && _isMapInitialized) {
_updateTrainMarkers();
}
});
Future.delayed(const Duration(seconds: 5), () {
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
});
}
LatLng? _parseDmsCoordinate(String? positionInfo) {
if (positionInfo == null ||
positionInfo.isEmpty ||
positionInfo == '<NUL>') {
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<TrainRecord> _getValidRecords() {
return _trainRecords.where((record) {
final coords = record.getCoordinates();
return coords['lat'] != 0.0 && coords['lng'] != 0.0;
}).toList();
}
List<TrainRecord> _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<String, dynamic>.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<String, dynamic> 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<String, dynamic> mapState) {
try {
final lat = mapState['lat'] as double;
final lng = mapState['lng'] as double;
final zoom = mapState['zoom'] as double;
final bearing = mapState['bearing'] as double;
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<void> _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>(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),
),
],
),
),
],
),
);
}
}