From c3e97332fd504d2c75d6dc70e24ed0882c01a733 Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Sat, 27 Sep 2025 00:14:24 +0800 Subject: [PATCH] feat: add map time filtering function and optimized location processing --- lib/screens/history_screen.dart | 200 +++++++++++----- lib/screens/map_screen.dart | 365 ++++++++++++++++++++++++----- lib/services/ble_service.dart | 4 + lib/services/database_service.dart | 42 +++- lib/services/merge_service.dart | 23 +- 5 files changed, 509 insertions(+), 125 deletions(-) diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index f2ae913..2b7fdc4 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import '../models/merged_record.dart'; import '../services/database_service.dart'; @@ -45,6 +46,10 @@ class HistoryScreenState extends State { final Map _mapOptimalZoom = {}; final Map _mapCalculating = {}; + LatLng? _currentUserLocation; + bool _isLocationPermissionGranted = false; + Timer? _locationTimer; + int getSelectedCount() => _selectedRecords.length; Set getSelectedRecordIds() => _selectedRecords; List getDisplayItems() => _displayItems; @@ -81,7 +86,10 @@ class HistoryScreenState extends State { } }); WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) loadRecords(); + if (mounted) { + loadRecords(); + _startLocationUpdates(); + } }); } @@ -89,6 +97,7 @@ class HistoryScreenState extends State { void dispose() { _scrollController.dispose(); _observerController.controller?.dispose(); + _locationTimer?.cancel(); super.dispose(); } @@ -660,6 +669,7 @@ class HistoryScreenState extends State { center: bounds.center, zoom: zoomLevel, groupKey: groupKey, + currentUserLocation: _currentUserLocation, )) ]); } @@ -670,6 +680,53 @@ class HistoryScreenState extends State { return 10.0; } + Future _requestLocationPermission() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return; + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.deniedForever) { + return; + } + + setState(() { + _isLocationPermissionGranted = true; + }); + + _getCurrentLocation(); + } + + Future _getCurrentLocation() async { + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + forceAndroidLocationManager: true, + ); + + setState(() { + _currentUserLocation = LatLng(position.latitude, position.longitude); + }); + } catch (e) { + print('获取当前位置失败: $e'); + } + } + + void _startLocationUpdates() { + _requestLocationPermission(); + + _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) { + if (_isLocationPermissionGranted) { + _getCurrentLocation(); + } + }); + } + Widget _buildRecordCard(TrainRecord record, {bool isSubCard = false, Key? key}) { final isSelected = _selectedRecords.contains(record.uniqueId); @@ -704,26 +761,6 @@ class HistoryScreenState extends State { } widget.onSelectionChanged(); }); - } else if (!isSubCard) { - if (isExpanded) { - final shouldUpdate = - _expandedStates[record.uniqueId] == true || - _mapOptimalZoom.containsKey(record.uniqueId) || - _mapCalculating.containsKey(record.uniqueId); - if (shouldUpdate) { - setState(() { - _expandedStates[record.uniqueId] = false; - _mapOptimalZoom.remove(record.uniqueId); - _mapCalculating.remove(record.uniqueId); - }); - } - } else { - if (_expandedStates[record.uniqueId] != true) { - setState(() { - _expandedStates[record.uniqueId] = true; - }); - } - } } }, onLongPress: () { @@ -961,6 +998,7 @@ class HistoryScreenState extends State { position: position, zoom: zoomLevel, recordId: record.uniqueId, + currentUserLocation: _currentUserLocation, )) ]); } @@ -1101,12 +1139,14 @@ class _DelayedMapWithMarker extends StatefulWidget { final LatLng position; final double zoom; final String recordId; + final LatLng? currentUserLocation; const _DelayedMapWithMarker({ Key? key, required this.position, required this.zoom, required this.recordId, + this.currentUserLocation, }) : super(key: key); @override @@ -1164,6 +1204,44 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> { @override Widget build(BuildContext context) { + final markers = [ + Marker( + point: widget.position, + width: 24, + height: 24, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white, width: 1.5), + ), + child: const Icon(Icons.train, color: Colors.white, size: 12), + ), + ), + ]; + + if (widget.currentUserLocation != null) { + markers.add( + Marker( + point: widget.currentUserLocation!, + width: 24, + height: 24, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1.5), + ), + child: const Icon( + Icons.my_location, + color: Colors.white, + size: 12, + ), + ), + ), + ); + } + if (_isInitializing) { return FlutterMap( options: MapOptions( @@ -1176,19 +1254,7 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> { TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'org.noxylva.lbjconsole'), - MarkerLayer(markers: [ - Marker( - point: widget.position, - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white, width: 2)), - child: - const Icon(Icons.train, color: Colors.white, size: 20))) - ]) + MarkerLayer(markers: markers), ], ); } @@ -1202,19 +1268,7 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> { TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'org.noxylva.lbjconsole'), - MarkerLayer(markers: [ - Marker( - point: widget.position, - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white, width: 2)), - child: - const Icon(Icons.train, color: Colors.white, size: 20))) - ]) + MarkerLayer(markers: markers), ], ); } @@ -1225,6 +1279,7 @@ class _DelayedMultiMarkerMap extends StatefulWidget { final LatLng center; final double zoom; final String groupKey; + final LatLng? currentUserLocation; const _DelayedMultiMarkerMap({ Key? key, @@ -1232,6 +1287,7 @@ class _DelayedMultiMarkerMap extends StatefulWidget { required this.center, required this.zoom, required this.groupKey, + this.currentUserLocation, }) : super(key: key); @override @@ -1291,6 +1347,41 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> { @override Widget build(BuildContext context) { + final markers = [ + ...widget.positions.map((pos) => Marker( + point: pos, + width: 24, + height: 24, + child: Container( + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1.5)), + child: const Icon(Icons.train, color: Colors.white, size: 12)))), + ]; + + if (widget.currentUserLocation != null) { + markers.add( + Marker( + point: widget.currentUserLocation!, + width: 24, + height: 24, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1.5), + ), + child: const Icon( + Icons.my_location, + color: Colors.white, + size: 12, + ), + ), + ), + ); + } + return FlutterMap( options: MapOptions( onPositionChanged: (position, hasGesture) => _onCameraMove(), @@ -1303,20 +1394,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> { urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'org.noxylva.lbjconsole', ), - MarkerLayer( - markers: widget.positions - .map((pos) => Marker( - point: pos, - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.8), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2)), - child: const Icon(Icons.train, - color: Colors.white, size: 20)))) - .toList()), + MarkerLayer(markers: markers), ], ); } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b4e9d6a..3c66342 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' show sin, cos, sqrt, atan2, pi; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -29,13 +30,89 @@ class _MapScreenState extends State { bool _isLocationPermissionGranted = false; Timer? _locationTimer; + String _selectedTimeFilter = 'unlimited'; + final Map _timeFilterOptions = { + 'unlimited': Duration.zero, + '1hour': Duration(hours: 1), + '6hours': Duration(hours: 6), + '12hours': Duration(hours: 12), + '24hours': Duration(hours: 24), + '7days': Duration(days: 7), + '30days': Duration(days: 30), + }; + @override void initState() { super.initState(); + print('=== 地图页面初始化 ==='); _initializeMap(); - _loadTrainRecords(); - _loadSettings(); - _startLocationUpdates(); + + _checkDatabaseSettings(); + + // + _loadSettings().then((_) { + print('设置加载完成,开始加载列车记录'); + _loadTrainRecords().then((_) { + print('列车记录加载完成,开始位置更新'); + _startLocationUpdates(); + }); + }); + } + + Future _checkDatabaseSettings() async { + try { + print('=== 检查数据库设置 ==='); + final dbInfo = await DatabaseService.instance.getDatabaseInfo(); + print('数据库信息: $dbInfo'); + + final settings = await DatabaseService.instance.getAllSettings(); + print('数据库设置详情: $settings'); + + if (settings != null) { + final lat = settings['mapCenterLat']; + final lon = settings['mapCenterLon']; + print('数据库中的位置坐标: lat=$lat, lon=$lon'); + + if (lat != null && lon != null) { + if (lat == 39.9042 && lon == 116.4074) { + print('警告:数据库中保存的是北京默认坐标'); + } else if (lat == 0.0 && lon == 0.0) { + print('警告:数据库中保存的是零坐标'); + } else { + print('数据库中保存的是有效坐标'); + final beijingLat = 39.9042; + final beijingLon = 116.4074; + final distance = + _calculateDistance(lat, lon, beijingLat, beijingLon); + print('与北京市中心的距离: ${distance.toStringAsFixed(2)} 公里'); + + if (distance < 50) { + print('注意:保存的位置在北京附近(距离 < 50公里)'); + } + } + } + } + } catch (e) { + print('检查数据库设置失败: $e'); + } + } + + double _calculateDistance( + double lat1, double lon1, double lat2, double lon2) { + const earthRadius = 6371; + final dLat = _degreesToRadians(lat2 - lat1); + final dLon = _degreesToRadians(lon2 - lon1); + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(_degreesToRadians(lat1)) * + cos(_degreesToRadians(lat2)) * + sin(dLon / 2) * + sin(dLon / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return earthRadius * c; + } + + double _degreesToRadians(double degrees) { + return degrees * pi / 180; } @override @@ -77,15 +154,25 @@ class _MapScreenState extends State { Future _getCurrentLocation() async { try { + print('=== 获取当前位置 ==='); Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, forceAndroidLocationManager: true, ); + final newLocation = LatLng(position.latitude, position.longitude); + print('获取到位置: $newLocation'); setState(() { - _userLocation = LatLng(position.latitude, position.longitude); + _userLocation = newLocation; }); - } catch (e) {} + + if (!_isMapInitialized) { + print('获取位置后尝试初始化地图'); + _initializeMapPosition(); + } + } catch (e) { + print('获取位置失败: $e'); + } } void _startLocationUpdates() { @@ -119,43 +206,83 @@ class _MapScreenState extends State { Future _loadSettings() async { try { + print('=== 开始加载设置 ==='); final settings = await DatabaseService.instance.getAllSettings(); + print('设置数据: $settings'); if (settings != null) { + print( + '设置中的位置: lat=${settings['mapCenterLat']}, lon=${settings['mapCenterLon']}'); + print('设置中的缩放: ${settings['mapZoomLevel']}'); setState(() { _railwayLayerVisible = (settings['mapRailwayLayerVisible'] as int?) == 1; _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0; _currentRotation = (settings['mapRotation'] as num?)?.toDouble() ?? 0.0; + _selectedTimeFilter = + settings['mapTimeFilter'] as String? ?? 'unlimited'; final lat = (settings['mapCenterLat'] as num?)?.toDouble(); final lon = (settings['mapCenterLon'] as num?)?.toDouble(); - if (lat != null && lon != null) { + if (lat != null && lon != null && lat != 0.0 && lon != 0.0) { _currentLocation = LatLng(lat, lon); + print('使用保存的位置: $_currentLocation'); + } else { + print('保存的位置无效或为零,不使用'); } }); + print('设置加载完成,当前位置: $_currentLocation'); + if (!_isMapInitialized) { + print('设置加载后尝试初始化地图'); + _initializeMapPosition(); + } + } else { + print('没有保存的设置数据'); } - } catch (e) {} + } catch (e) { + print('加载设置失败: $e'); + } } Future _saveSettings() async { try { + print('=== 保存设置到数据库 ==='); + print('当前旋转角度: $_currentRotation'); + print('当前缩放级别: $_currentZoom'); + print('当前位置: $_currentLocation'); + final center = _mapController.camera.center; - await DatabaseService.instance.updateSettings({ + + final isDefaultLocation = + center.latitude == 39.9042 && center.longitude == 116.4074; + + final settings = { 'mapRailwayLayerVisible': _railwayLayerVisible ? 1 : 0, 'mapZoomLevel': _currentZoom, - 'mapCenterLat': center.latitude, - 'mapCenterLon': center.longitude, 'mapRotation': _currentRotation, - }); - } catch (e) {} + 'mapTimeFilter': _selectedTimeFilter, + }; + + if (!isDefaultLocation) { + settings['mapCenterLat'] = center.latitude; + settings['mapCenterLon'] = center.longitude; + } + + print('保存的设置数据: $settings'); + await DatabaseService.instance.updateSettings(settings); + print('=== 设置保存成功 ==='); + } catch (e) { + print('保存设置失败: $e'); + } } Future _loadTrainRecords() async { setState(() => _isLoading = true); try { - final records = await DatabaseService.instance.getAllRecords(); + print('=== 开始加载列车记录 ==='); + final records = await _getFilteredRecords(); + print('加载到 ${records.length} 条记录'); setState(() { _trainRecords.clear(); _trainRecords.addAll(records); @@ -163,20 +290,46 @@ class _MapScreenState extends State { if (_trainRecords.isNotEmpty) { final lastRecord = _trainRecords.first; + print( + '最新记录: ${lastRecord.fullTrainNumber}, 位置: ${lastRecord.position}'); final coords = lastRecord.getCoordinates(); final dmsCoords = _parseDmsCoordinate(lastRecord.positionInfo); if (dmsCoords != null) { _lastTrainLocation = dmsCoords; + print('使用DMS坐标: $dmsCoords'); } else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { _lastTrainLocation = LatLng(coords['lat']!, coords['lng']!); + print('使用解析坐标: $_lastTrainLocation'); + } else { + print('记录中没有有效坐标'); } + } else { + print('没有列车记录'); } - _initializeMapPosition(); + print('列车位置: $_lastTrainLocation'); + if (!_isMapInitialized) { + print('列车记录加载后尝试初始化地图'); + _initializeMapPosition(); + } }); } catch (e) { setState(() => _isLoading = false); + print('加载列车记录失败: $e'); + } + } + + Future> _getFilteredRecords() async { + if (_selectedTimeFilter == 'unlimited') { + return await DatabaseService.instance.getAllRecords(); + } else { + final duration = _timeFilterOptions[_selectedTimeFilter]; + if (duration != null && duration != Duration.zero) { + return await DatabaseService.instance + .getRecordsWithinReceivedTimeRange(duration); + } + return await DatabaseService.instance.getAllRecords(); } } @@ -185,23 +338,36 @@ class _MapScreenState extends State { LatLng? targetLocation; + print('=== 初始化地图位置 ==='); + print('当前位置: $_currentLocation'); + print('列车位置: $_lastTrainLocation'); + print('用户位置: $_userLocation'); + print('地图已初始化: $_isMapInitialized'); + if (_currentLocation != null) { targetLocation = _currentLocation; - } else if (_userLocation != null) { - targetLocation = _userLocation; + print('使用保存的坐标: $targetLocation'); } else if (_lastTrainLocation != null) { targetLocation = _lastTrainLocation; + print('使用列车位置: $targetLocation'); + } else if (_userLocation != null) { + targetLocation = _userLocation; + print('使用用户位置: $targetLocation'); } else { - _isMapInitialized = true; - return; + targetLocation = const LatLng(39.9042, 116.4074); + print('没有可用位置,使用北京默认位置: $targetLocation'); } - _centerMap(targetLocation!, zoom: _currentZoom); + print('最终选择位置: $targetLocation'); + print('当前旋转角度: $_currentRotation'); + _centerMap(targetLocation!, zoom: _currentZoom, rotation: _currentRotation); _isMapInitialized = true; + print('地图初始化完成,旋转角度: $_currentRotation'); } - void _centerMap(LatLng location, {double? zoom}) { + void _centerMap(LatLng location, {double? zoom, double? rotation}) { _mapController.move(location, zoom ?? _currentZoom); + _mapController.rotate(rotation ?? _currentRotation); } LatLng? _parseDmsCoordinate(String? positionInfo) { @@ -292,41 +458,27 @@ class _MapScreenState extends State { Marker( point: position, width: 80, - height: 60, + height: 16, child: GestureDetector( onTap: () => position != null ? _showTrainDetailsDialog(record, position) : null, child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, 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), + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(2), + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(3), ), child: Text( trainDisplay, style: const TextStyle( color: Colors.white, - fontSize: 10, + fontSize: 8, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, @@ -345,8 +497,9 @@ class _MapScreenState extends State { } void _centerToMyLocation() { - _centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), - zoom: 15.0); + if (_userLocation != null) { + _centerMap(_userLocation!, zoom: 15.0, rotation: _currentRotation); + } } void _centerToLastTrain() { @@ -363,11 +516,73 @@ class _MapScreenState extends State { } if (targetPosition != null) { - _centerMap(targetPosition, zoom: 15.0); + _centerMap(targetPosition, zoom: 15.0, rotation: _currentRotation); } } } + void _showTimeFilterDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('时间筛选'), + content: SizedBox( + width: double.minPositive, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _timeFilterOptions.keys.map((key) { + return RadioListTile( + title: Text(_getTimeFilterLabel(key)), + value: key, + groupValue: _selectedTimeFilter, + onChanged: (String? value) { + if (value != null) { + setState(() { + _selectedTimeFilter = value; + }); + _loadTrainRecords(); + Navigator.pop(context); + } + }, + contentPadding: EdgeInsets.zero, + dense: true, + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ], + ); + }, + ); + } + + String _getTimeFilterLabel(String key) { + switch (key) { + case 'unlimited': + return '全部时间'; + 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 _showTrainDetailsDialog(TrainRecord record, LatLng position) { showModalBottomSheet( context: context, @@ -428,9 +643,9 @@ class _MapScreenState extends State { child: Column( children: [ _buildMaterial3DetailRow( - context, "时间", record.formattedTime), + context, "时间", _getDisplayTime(record)), _buildMaterial3DetailRow( - context, "日期", record.formattedDate), + context, "日期", _getDisplayDate(record)), _buildMaterial3DetailRow( context, "类型", record.trainType), _buildMaterial3DetailRow(context, "速度", @@ -470,7 +685,8 @@ class _MapScreenState extends State { child: FilledButton( onPressed: () { Navigator.pop(context); - _centerMap(position, zoom: 17.0); + _centerMap(position, + zoom: 17.0, rotation: _currentRotation); }, child: const Row( mainAxisAlignment: MainAxisAlignment.center, @@ -516,6 +732,25 @@ class _MapScreenState extends State { ); } + String _getDisplayTime(TrainRecord record) { + if (record.time == "" || record.time.isEmpty) { + final receivedTime = record.receivedTimestamp; + return '${receivedTime.hour.toString().padLeft(2, '0')}:${receivedTime.minute.toString().padLeft(2, '0')}:${receivedTime.second.toString().padLeft(2, '0')}'; + } else { + return record.time.split("\n")[0]; + } + } + + String _getDisplayDate(TrainRecord record) { + if (record.time == "" || record.time.isEmpty) { + final receivedTime = record.receivedTimestamp; + return '${receivedTime.year}-${receivedTime.month.toString().padLeft(2, '0')}-${receivedTime.day.toString().padLeft(2, '0')}'; + } else { + final now = DateTime.now(); + return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; + } + } + Widget _buildMaterial3DetailRow( BuildContext context, String label, String value) { return Padding( @@ -555,18 +790,18 @@ class _MapScreenState extends State { markers.add( Marker( point: _userLocation!, - width: 40, - height: 40, + width: 24, + height: 24, child: Container( decoration: BoxDecoration( color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white, width: 1), ), child: const Icon( Icons.my_location, color: Colors.white, - size: 20, + size: 12, ), ), ), @@ -580,21 +815,22 @@ class _MapScreenState extends State { FlutterMap( mapController: _mapController, options: MapOptions( - initialCenter: - _lastTrainLocation ?? const LatLng(39.9042, 116.4074), + initialCenter: _currentLocation ?? + _lastTrainLocation ?? + _userLocation ?? + 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(); - } + setState(() { + _currentLocation = camera.center; + _currentZoom = camera.zoom; + _currentRotation = camera.rotation; + }); + + _saveSettings(); }, ), children: [ @@ -625,6 +861,13 @@ class _MapScreenState extends State { top: 40, child: Column( children: [ + FloatingActionButton.small( + heroTag: 'timeFilter', + backgroundColor: const Color(0xFF1E1E1E), + onPressed: _showTimeFilterDialog, + child: const Icon(Icons.filter_list, color: Colors.white), + ), + const SizedBox(height: 8), FloatingActionButton.small( heroTag: 'railwayLayer', backgroundColor: const Color(0xFF1E1E1E), diff --git a/lib/services/ble_service.dart b/lib/services/ble_service.dart index 76aed99..6fc4722 100644 --- a/lib/services/ble_service.dart +++ b/lib/services/ble_service.dart @@ -319,6 +319,10 @@ class BLEService { '${now.millisecondsSinceEpoch}_${Random().nextInt(9999)}'; recordData['receivedTimestamp'] = now.millisecondsSinceEpoch; + if (!recordData.containsKey('timestamp')) { + recordData['timestamp'] = now.millisecondsSinceEpoch; + } + _lastReceivedTime = now; _lastReceivedTimeController.add(_lastReceivedTime); diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart index 2bce1c7..d9a3a16 100644 --- a/lib/services/database_service.dart +++ b/lib/services/database_service.dart @@ -13,7 +13,7 @@ class DatabaseService { DatabaseService._internal(); static const String _databaseName = 'train_database'; - static const _databaseVersion = 2; + static const _databaseVersion = 4; static const String trainRecordsTable = 'train_records'; static const String appSettingsTable = 'app_settings'; @@ -43,6 +43,17 @@ class DatabaseService { await db.execute( 'ALTER TABLE $appSettingsTable ADD COLUMN hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0'); } + if (oldVersion < 3) { + await db.execute( + 'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"'); + } + if (oldVersion < 4) { + try { + await db.execute( + 'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"'); + } catch (e) { + } + } } Future _onCreate(Database db, int version) async { @@ -89,7 +100,8 @@ class DatabaseService { mergeRecordsEnabled INTEGER NOT NULL DEFAULT 0, hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0, groupBy TEXT NOT NULL DEFAULT 'trainAndLoco', - timeWindow TEXT NOT NULL DEFAULT 'unlimited' + timeWindow TEXT NOT NULL DEFAULT 'unlimited', + mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited' ) '''); @@ -114,6 +126,7 @@ class DatabaseService { 'hideTimeOnlyRecords': 0, 'groupBy': 'trainAndLoco', 'timeWindow': 'unlimited', + 'mapTimeFilter': 'unlimited', }); } @@ -135,6 +148,31 @@ class DatabaseService { return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); } + Future> getRecordsWithinTimeRange(Duration duration) async { + final db = await database; + final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch; + final result = await db.query( + trainRecordsTable, + where: 'timestamp >= ?', + whereArgs: [cutoffTime], + orderBy: 'timestamp DESC', + ); + return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + } + + Future> getRecordsWithinReceivedTimeRange( + Duration duration) async { + final db = await database; + final cutoffTime = DateTime.now().subtract(duration).millisecondsSinceEpoch; + final result = await db.query( + trainRecordsTable, + where: 'receivedTimestamp >= ?', + whereArgs: [cutoffTime], + orderBy: 'receivedTimestamp DESC', + ); + return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); + } + Future deleteRecord(String uniqueId) async { final db = await database; return await db.delete( diff --git a/lib/services/merge_service.dart b/lib/services/merge_service.dart index 4b7274b..f5e3cf4 100644 --- a/lib/services/merge_service.dart +++ b/lib/services/merge_service.dart @@ -142,8 +142,29 @@ class MergeService { } if (group.length >= 2) { + final firstRecord = group.first; + final train = firstRecord.train.trim(); + final loco = firstRecord.loco.trim(); + String uniqueGroupKey; + + if (train.isNotEmpty && + train != "" && + !train.contains("-----") && + loco.isNotEmpty && + loco != "") { + uniqueGroupKey = "train_or_loco:${train}_$loco"; + } else if (train.isNotEmpty && + train != "" && + !train.contains("-----")) { + uniqueGroupKey = "train_or_loco:train:$train"; + } else if (loco.isNotEmpty && loco != "") { + uniqueGroupKey = "train_or_loco:loco:$loco"; + } else { + uniqueGroupKey = "train_or_loco:group_${mergedRecords.length}"; + } + mergedRecords.add(MergedTrainRecord( - groupKey: "train_or_loco_group", + groupKey: uniqueGroupKey, records: group, latestRecord: group.first, ));