feat: add map time filtering function and optimized location processing

This commit is contained in:
Nedifinita
2025-09-27 00:14:24 +08:00
parent b1d8d5e029
commit c3e97332fd
5 changed files with 509 additions and 125 deletions

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';
import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/scrollview_observer.dart';
import '../models/merged_record.dart'; import '../models/merged_record.dart';
import '../services/database_service.dart'; import '../services/database_service.dart';
@@ -45,6 +46,10 @@ class HistoryScreenState extends State<HistoryScreen> {
final Map<String, double> _mapOptimalZoom = {}; final Map<String, double> _mapOptimalZoom = {};
final Map<String, bool> _mapCalculating = {}; final Map<String, bool> _mapCalculating = {};
LatLng? _currentUserLocation;
bool _isLocationPermissionGranted = false;
Timer? _locationTimer;
int getSelectedCount() => _selectedRecords.length; int getSelectedCount() => _selectedRecords.length;
Set<String> getSelectedRecordIds() => _selectedRecords; Set<String> getSelectedRecordIds() => _selectedRecords;
List<Object> getDisplayItems() => _displayItems; List<Object> getDisplayItems() => _displayItems;
@@ -81,7 +86,10 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) loadRecords(); if (mounted) {
loadRecords();
_startLocationUpdates();
}
}); });
} }
@@ -89,6 +97,7 @@ class HistoryScreenState extends State<HistoryScreen> {
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();
_observerController.controller?.dispose(); _observerController.controller?.dispose();
_locationTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -660,6 +669,7 @@ class HistoryScreenState extends State<HistoryScreen> {
center: bounds.center, center: bounds.center,
zoom: zoomLevel, zoom: zoomLevel,
groupKey: groupKey, groupKey: groupKey,
currentUserLocation: _currentUserLocation,
)) ))
]); ]);
} }
@@ -670,6 +680,53 @@ class HistoryScreenState extends State<HistoryScreen> {
return 10.0; return 10.0;
} }
Future<void> _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<void> _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, Widget _buildRecordCard(TrainRecord record,
{bool isSubCard = false, Key? key}) { {bool isSubCard = false, Key? key}) {
final isSelected = _selectedRecords.contains(record.uniqueId); final isSelected = _selectedRecords.contains(record.uniqueId);
@@ -704,26 +761,6 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
widget.onSelectionChanged(); 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: () { onLongPress: () {
@@ -961,6 +998,7 @@ class HistoryScreenState extends State<HistoryScreen> {
position: position, position: position,
zoom: zoomLevel, zoom: zoomLevel,
recordId: record.uniqueId, recordId: record.uniqueId,
currentUserLocation: _currentUserLocation,
)) ))
]); ]);
} }
@@ -1101,12 +1139,14 @@ class _DelayedMapWithMarker extends StatefulWidget {
final LatLng position; final LatLng position;
final double zoom; final double zoom;
final String recordId; final String recordId;
final LatLng? currentUserLocation;
const _DelayedMapWithMarker({ const _DelayedMapWithMarker({
Key? key, Key? key,
required this.position, required this.position,
required this.zoom, required this.zoom,
required this.recordId, required this.recordId,
this.currentUserLocation,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -1164,6 +1204,44 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final markers = <Marker>[
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) { if (_isInitializing) {
return FlutterMap( return FlutterMap(
options: MapOptions( options: MapOptions(
@@ -1176,19 +1254,7 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'org.noxylva.lbjconsole'), userAgentPackageName: 'org.noxylva.lbjconsole'),
MarkerLayer(markers: [ MarkerLayer(markers: 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)))
])
], ],
); );
} }
@@ -1202,19 +1268,7 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'org.noxylva.lbjconsole'), userAgentPackageName: 'org.noxylva.lbjconsole'),
MarkerLayer(markers: [ MarkerLayer(markers: 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)))
])
], ],
); );
} }
@@ -1225,6 +1279,7 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
final LatLng center; final LatLng center;
final double zoom; final double zoom;
final String groupKey; final String groupKey;
final LatLng? currentUserLocation;
const _DelayedMultiMarkerMap({ const _DelayedMultiMarkerMap({
Key? key, Key? key,
@@ -1232,6 +1287,7 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
required this.center, required this.center,
required this.zoom, required this.zoom,
required this.groupKey, required this.groupKey,
this.currentUserLocation,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -1291,6 +1347,41 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final markers = <Marker>[
...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( return FlutterMap(
options: MapOptions( options: MapOptions(
onPositionChanged: (position, hasGesture) => _onCameraMove(), onPositionChanged: (position, hasGesture) => _onCameraMove(),
@@ -1303,20 +1394,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'org.noxylva.lbjconsole', userAgentPackageName: 'org.noxylva.lbjconsole',
), ),
MarkerLayer( MarkerLayer(markers: markers),
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()),
], ],
); );
} }

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' show sin, cos, sqrt, atan2, pi;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@@ -29,13 +30,89 @@ class _MapScreenState extends State<MapScreen> {
bool _isLocationPermissionGranted = false; bool _isLocationPermissionGranted = false;
Timer? _locationTimer; Timer? _locationTimer;
String _selectedTimeFilter = 'unlimited';
final Map<String, Duration> _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 @override
void initState() { void initState() {
super.initState(); super.initState();
print('=== 地图页面初始化 ===');
_initializeMap(); _initializeMap();
_loadTrainRecords();
_loadSettings(); _checkDatabaseSettings();
_startLocationUpdates();
//
_loadSettings().then((_) {
print('设置加载完成,开始加载列车记录');
_loadTrainRecords().then((_) {
print('列车记录加载完成,开始位置更新');
_startLocationUpdates();
});
});
}
Future<void> _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 @override
@@ -77,15 +154,25 @@ class _MapScreenState extends State<MapScreen> {
Future<void> _getCurrentLocation() async { Future<void> _getCurrentLocation() async {
try { try {
print('=== 获取当前位置 ===');
Position position = await Geolocator.getCurrentPosition( Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high, desiredAccuracy: LocationAccuracy.high,
forceAndroidLocationManager: true, forceAndroidLocationManager: true,
); );
final newLocation = LatLng(position.latitude, position.longitude);
print('获取到位置: $newLocation');
setState(() { setState(() {
_userLocation = LatLng(position.latitude, position.longitude); _userLocation = newLocation;
}); });
} catch (e) {}
if (!_isMapInitialized) {
print('获取位置后尝试初始化地图');
_initializeMapPosition();
}
} catch (e) {
print('获取位置失败: $e');
}
} }
void _startLocationUpdates() { void _startLocationUpdates() {
@@ -119,43 +206,83 @@ class _MapScreenState extends State<MapScreen> {
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
try { try {
print('=== 开始加载设置 ===');
final settings = await DatabaseService.instance.getAllSettings(); final settings = await DatabaseService.instance.getAllSettings();
print('设置数据: $settings');
if (settings != null) { if (settings != null) {
print(
'设置中的位置: lat=${settings['mapCenterLat']}, lon=${settings['mapCenterLon']}');
print('设置中的缩放: ${settings['mapZoomLevel']}');
setState(() { setState(() {
_railwayLayerVisible = _railwayLayerVisible =
(settings['mapRailwayLayerVisible'] as int?) == 1; (settings['mapRailwayLayerVisible'] as int?) == 1;
_currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0; _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0;
_currentRotation = _currentRotation =
(settings['mapRotation'] as num?)?.toDouble() ?? 0.0; (settings['mapRotation'] as num?)?.toDouble() ?? 0.0;
_selectedTimeFilter =
settings['mapTimeFilter'] as String? ?? 'unlimited';
final lat = (settings['mapCenterLat'] as num?)?.toDouble(); final lat = (settings['mapCenterLat'] as num?)?.toDouble();
final lon = (settings['mapCenterLon'] 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); _currentLocation = LatLng(lat, lon);
print('使用保存的位置: $_currentLocation');
} else {
print('保存的位置无效或为零,不使用');
} }
}); });
print('设置加载完成,当前位置: $_currentLocation');
if (!_isMapInitialized) {
print('设置加载后尝试初始化地图');
_initializeMapPosition();
}
} else {
print('没有保存的设置数据');
} }
} catch (e) {} } catch (e) {
print('加载设置失败: $e');
}
} }
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
try { try {
print('=== 保存设置到数据库 ===');
print('当前旋转角度: $_currentRotation');
print('当前缩放级别: $_currentZoom');
print('当前位置: $_currentLocation');
final center = _mapController.camera.center; 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, 'mapRailwayLayerVisible': _railwayLayerVisible ? 1 : 0,
'mapZoomLevel': _currentZoom, 'mapZoomLevel': _currentZoom,
'mapCenterLat': center.latitude,
'mapCenterLon': center.longitude,
'mapRotation': _currentRotation, 'mapRotation': _currentRotation,
}); 'mapTimeFilter': _selectedTimeFilter,
} catch (e) {} };
if (!isDefaultLocation) {
settings['mapCenterLat'] = center.latitude;
settings['mapCenterLon'] = center.longitude;
}
print('保存的设置数据: $settings');
await DatabaseService.instance.updateSettings(settings);
print('=== 设置保存成功 ===');
} catch (e) {
print('保存设置失败: $e');
}
} }
Future<void> _loadTrainRecords() async { Future<void> _loadTrainRecords() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final records = await DatabaseService.instance.getAllRecords(); print('=== 开始加载列车记录 ===');
final records = await _getFilteredRecords();
print('加载到 ${records.length} 条记录');
setState(() { setState(() {
_trainRecords.clear(); _trainRecords.clear();
_trainRecords.addAll(records); _trainRecords.addAll(records);
@@ -163,20 +290,46 @@ class _MapScreenState extends State<MapScreen> {
if (_trainRecords.isNotEmpty) { if (_trainRecords.isNotEmpty) {
final lastRecord = _trainRecords.first; final lastRecord = _trainRecords.first;
print(
'最新记录: ${lastRecord.fullTrainNumber}, 位置: ${lastRecord.position}');
final coords = lastRecord.getCoordinates(); final coords = lastRecord.getCoordinates();
final dmsCoords = _parseDmsCoordinate(lastRecord.positionInfo); final dmsCoords = _parseDmsCoordinate(lastRecord.positionInfo);
if (dmsCoords != null) { if (dmsCoords != null) {
_lastTrainLocation = dmsCoords; _lastTrainLocation = dmsCoords;
print('使用DMS坐标: $dmsCoords');
} else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { } else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) {
_lastTrainLocation = LatLng(coords['lat']!, coords['lng']!); _lastTrainLocation = LatLng(coords['lat']!, coords['lng']!);
print('使用解析坐标: $_lastTrainLocation');
} else {
print('记录中没有有效坐标');
} }
} else {
print('没有列车记录');
} }
_initializeMapPosition(); print('列车位置: $_lastTrainLocation');
if (!_isMapInitialized) {
print('列车记录加载后尝试初始化地图');
_initializeMapPosition();
}
}); });
} catch (e) { } catch (e) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
print('加载列车记录失败: $e');
}
}
Future<List<TrainRecord>> _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<MapScreen> {
LatLng? targetLocation; LatLng? targetLocation;
print('=== 初始化地图位置 ===');
print('当前位置: $_currentLocation');
print('列车位置: $_lastTrainLocation');
print('用户位置: $_userLocation');
print('地图已初始化: $_isMapInitialized');
if (_currentLocation != null) { if (_currentLocation != null) {
targetLocation = _currentLocation; targetLocation = _currentLocation;
} else if (_userLocation != null) { print('使用保存的坐标: $targetLocation');
targetLocation = _userLocation;
} else if (_lastTrainLocation != null) { } else if (_lastTrainLocation != null) {
targetLocation = _lastTrainLocation; targetLocation = _lastTrainLocation;
print('使用列车位置: $targetLocation');
} else if (_userLocation != null) {
targetLocation = _userLocation;
print('使用用户位置: $targetLocation');
} else { } else {
_isMapInitialized = true; targetLocation = const LatLng(39.9042, 116.4074);
return; print('没有可用位置,使用北京默认位置: $targetLocation');
} }
_centerMap(targetLocation!, zoom: _currentZoom); print('最终选择位置: $targetLocation');
print('当前旋转角度: $_currentRotation');
_centerMap(targetLocation!, zoom: _currentZoom, rotation: _currentRotation);
_isMapInitialized = true; _isMapInitialized = true;
print('地图初始化完成,旋转角度: $_currentRotation');
} }
void _centerMap(LatLng location, {double? zoom}) { void _centerMap(LatLng location, {double? zoom, double? rotation}) {
_mapController.move(location, zoom ?? _currentZoom); _mapController.move(location, zoom ?? _currentZoom);
_mapController.rotate(rotation ?? _currentRotation);
} }
LatLng? _parseDmsCoordinate(String? positionInfo) { LatLng? _parseDmsCoordinate(String? positionInfo) {
@@ -292,41 +458,27 @@ class _MapScreenState extends State<MapScreen> {
Marker( Marker(
point: position, point: position,
width: 80, width: 80,
height: 60, height: 16,
child: GestureDetector( child: GestureDetector(
onTap: () => position != null onTap: () => position != null
? _showTrainDetailsDialog(record, position) ? _showTrainDetailsDialog(record, position)
: null, : null,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ 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( Container(
padding: padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 1), const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7), color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(3),
), ),
child: Text( child: Text(
trainDisplay, trainDisplay,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 10, fontSize: 8,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -345,8 +497,9 @@ class _MapScreenState extends State<MapScreen> {
} }
void _centerToMyLocation() { void _centerToMyLocation() {
_centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), if (_userLocation != null) {
zoom: 15.0); _centerMap(_userLocation!, zoom: 15.0, rotation: _currentRotation);
}
} }
void _centerToLastTrain() { void _centerToLastTrain() {
@@ -363,11 +516,73 @@ class _MapScreenState extends State<MapScreen> {
} }
if (targetPosition != null) { 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<String>(
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) { void _showTrainDetailsDialog(TrainRecord record, LatLng position) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -428,9 +643,9 @@ class _MapScreenState extends State<MapScreen> {
child: Column( child: Column(
children: [ children: [
_buildMaterial3DetailRow( _buildMaterial3DetailRow(
context, "时间", record.formattedTime), context, "时间", _getDisplayTime(record)),
_buildMaterial3DetailRow( _buildMaterial3DetailRow(
context, "日期", record.formattedDate), context, "日期", _getDisplayDate(record)),
_buildMaterial3DetailRow( _buildMaterial3DetailRow(
context, "类型", record.trainType), context, "类型", record.trainType),
_buildMaterial3DetailRow(context, "速度", _buildMaterial3DetailRow(context, "速度",
@@ -470,7 +685,8 @@ class _MapScreenState extends State<MapScreen> {
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
_centerMap(position, zoom: 17.0); _centerMap(position,
zoom: 17.0, rotation: _currentRotation);
}, },
child: const Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -516,6 +732,25 @@ class _MapScreenState extends State<MapScreen> {
); );
} }
String _getDisplayTime(TrainRecord record) {
if (record.time == "<NUL>" || 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 == "<NUL>" || 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( Widget _buildMaterial3DetailRow(
BuildContext context, String label, String value) { BuildContext context, String label, String value) {
return Padding( return Padding(
@@ -555,18 +790,18 @@ class _MapScreenState extends State<MapScreen> {
markers.add( markers.add(
Marker( Marker(
point: _userLocation!, point: _userLocation!,
width: 40, width: 24,
height: 40, height: 24,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: Colors.blue,
shape: BoxShape.circle, borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 1),
), ),
child: const Icon( child: const Icon(
Icons.my_location, Icons.my_location,
color: Colors.white, color: Colors.white,
size: 20, size: 12,
), ),
), ),
), ),
@@ -580,21 +815,22 @@ class _MapScreenState extends State<MapScreen> {
FlutterMap( FlutterMap(
mapController: _mapController, mapController: _mapController,
options: MapOptions( options: MapOptions(
initialCenter: initialCenter: _currentLocation ??
_lastTrainLocation ?? const LatLng(39.9042, 116.4074), _lastTrainLocation ??
_userLocation ??
const LatLng(39.9042, 116.4074),
initialZoom: _currentZoom, initialZoom: _currentZoom,
initialRotation: _currentRotation, initialRotation: _currentRotation,
minZoom: 4.0, minZoom: 4.0,
maxZoom: 18.0, maxZoom: 18.0,
onPositionChanged: (MapCamera camera, bool hasGesture) { onPositionChanged: (MapCamera camera, bool hasGesture) {
if (hasGesture) { setState(() {
setState(() { _currentLocation = camera.center;
_currentLocation = camera.center; _currentZoom = camera.zoom;
_currentZoom = camera.zoom; _currentRotation = camera.rotation;
_currentRotation = camera.rotation; });
});
_saveSettings(); _saveSettings();
}
}, },
), ),
children: [ children: [
@@ -625,6 +861,13 @@ class _MapScreenState extends State<MapScreen> {
top: 40, top: 40,
child: Column( child: Column(
children: [ 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( FloatingActionButton.small(
heroTag: 'railwayLayer', heroTag: 'railwayLayer',
backgroundColor: const Color(0xFF1E1E1E), backgroundColor: const Color(0xFF1E1E1E),

View File

@@ -319,6 +319,10 @@ class BLEService {
'${now.millisecondsSinceEpoch}_${Random().nextInt(9999)}'; '${now.millisecondsSinceEpoch}_${Random().nextInt(9999)}';
recordData['receivedTimestamp'] = now.millisecondsSinceEpoch; recordData['receivedTimestamp'] = now.millisecondsSinceEpoch;
if (!recordData.containsKey('timestamp')) {
recordData['timestamp'] = now.millisecondsSinceEpoch;
}
_lastReceivedTime = now; _lastReceivedTime = now;
_lastReceivedTimeController.add(_lastReceivedTime); _lastReceivedTimeController.add(_lastReceivedTime);

View File

@@ -13,7 +13,7 @@ class DatabaseService {
DatabaseService._internal(); DatabaseService._internal();
static const String _databaseName = 'train_database'; static const String _databaseName = 'train_database';
static const _databaseVersion = 2; static const _databaseVersion = 4;
static const String trainRecordsTable = 'train_records'; static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings'; static const String appSettingsTable = 'app_settings';
@@ -43,6 +43,17 @@ class DatabaseService {
await db.execute( await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0'); '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<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
@@ -89,7 +100,8 @@ class DatabaseService {
mergeRecordsEnabled INTEGER NOT NULL DEFAULT 0, mergeRecordsEnabled INTEGER NOT NULL DEFAULT 0,
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0, hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco', 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, 'hideTimeOnlyRecords': 0,
'groupBy': 'trainAndLoco', 'groupBy': 'trainAndLoco',
'timeWindow': 'unlimited', 'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited',
}); });
} }
@@ -135,6 +148,31 @@ class DatabaseService {
return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList(); return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
} }
Future<List<TrainRecord>> 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<List<TrainRecord>> 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<int> deleteRecord(String uniqueId) async { Future<int> deleteRecord(String uniqueId) async {
final db = await database; final db = await database;
return await db.delete( return await db.delete(

View File

@@ -142,8 +142,29 @@ class MergeService {
} }
if (group.length >= 2) { if (group.length >= 2) {
final firstRecord = group.first;
final train = firstRecord.train.trim();
final loco = firstRecord.loco.trim();
String uniqueGroupKey;
if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----") &&
loco.isNotEmpty &&
loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:${train}_$loco";
} else if (train.isNotEmpty &&
train != "<NUL>" &&
!train.contains("-----")) {
uniqueGroupKey = "train_or_loco:train:$train";
} else if (loco.isNotEmpty && loco != "<NUL>") {
uniqueGroupKey = "train_or_loco:loco:$loco";
} else {
uniqueGroupKey = "train_or_loco:group_${mergedRecords.length}";
}
mergedRecords.add(MergedTrainRecord( mergedRecords.add(MergedTrainRecord(
groupKey: "train_or_loco_group", groupKey: uniqueGroupKey,
records: group, records: group,
latestRecord: group.first, latestRecord: group.first,
)); ));