diff --git a/android/app/build.gradle b/android/app/build.gradle
index c5b46c3..edfdc22 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -26,7 +26,7 @@ if (flutterVersionName == null) {
android {
namespace = "org.noxylva.lbjconsole.flutter"
compileSdk = 36
- ndkVersion = "26.1.10909125"
+ ndkVersion = "28.1.13356709"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/assets/mapbox_map.html b/assets/mapbox_map.html
new file mode 100644
index 0000000..eb47f2e
--- /dev/null
+++ b/assets/mapbox_map.html
@@ -0,0 +1,15038 @@
+
+
+
+
+ LBJ Console Railway Map
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/lib/models/merged_record.dart b/lib/models/merged_record.dart
index 6365f27..da75a5b 100644
--- a/lib/models/merged_record.dart
+++ b/lib/models/merged_record.dart
@@ -18,11 +18,12 @@ class MergeSettings {
final bool enabled;
final GroupBy groupBy;
final TimeWindow timeWindow;
-
+ final bool hideUngroupableRecords;
MergeSettings({
this.enabled = true,
this.groupBy = GroupBy.trainAndLoco,
this.timeWindow = TimeWindow.unlimited,
+ this.hideUngroupableRecords = false,
});
factory MergeSettings.fromMap(Map map) {
@@ -36,6 +37,7 @@ class MergeSettings {
(e) => e.name == map['timeWindow'],
orElse: () => TimeWindow.unlimited,
),
+ hideUngroupableRecords: (map['hideUngroupableRecords'] ?? 0) == 1,
);
}
}
diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart
index 128dfa2..28a47c4 100644
--- a/lib/screens/history_screen.dart
+++ b/lib/screens/history_screen.dart
@@ -1383,7 +1383,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
return FlutterMap(
options: MapOptions(
onPositionChanged: (position, hasGesture) => _onCameraMove(),
- minZoom: 5,
+ minZoom: 8,
maxZoom: 18,
),
mapController: _mapController,
diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart
index 5f4a606..7829bb1 100644
--- a/lib/screens/main_screen.dart
+++ b/lib/screens/main_screen.dart
@@ -6,6 +6,7 @@ import 'package:lbjconsole/models/merged_record.dart';
import 'package:lbjconsole/models/train_record.dart';
import 'package:lbjconsole/screens/history_screen.dart';
import 'package:lbjconsole/screens/map_screen.dart';
+import 'package:lbjconsole/screens/map_webview_screen.dart';
import 'package:lbjconsole/screens/settings_screen.dart';
import 'package:lbjconsole/services/ble_service.dart';
import 'package:lbjconsole/services/database_service.dart';
@@ -174,6 +175,7 @@ class MainScreen extends StatefulWidget {
class _MainScreenState extends State with WidgetsBindingObserver {
int _currentIndex = 0;
+ String _mapType = 'webview';
late final BLEService _bleService;
final NotificationService _notificationService = NotificationService();
@@ -195,8 +197,19 @@ class _MainScreenState extends State with WidgetsBindingObserver {
_initializeServices();
_checkAndStartBackgroundService();
_setupLastReceivedTimeListener();
+ _loadMapType();
}
+ Future _loadMapType() async {
+ final settings = await DatabaseService.instance.getAllSettings() ?? {};
+ if (mounted) {
+ setState(() {
+ _mapType = settings['mapType']?.toString() ?? 'webview';
+ });
+ }
+ }
+
+
Future _checkAndStartBackgroundService() async {
final settings = await DatabaseService.instance.getAllSettings() ?? {};
final backgroundServiceEnabled =
@@ -231,6 +244,7 @@ class _MainScreenState extends State with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_bleService.onAppResume();
+ _loadMapType();
}
}
@@ -380,8 +394,12 @@ class _MainScreenState extends State with WidgetsBindingObserver {
onEditModeChanged: _handleHistoryEditModeChanged,
onSelectionChanged: _handleSelectionChanged,
),
- const MapScreen(),
- const SettingsScreen(),
+ _mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
+ SettingsScreen(
+ onSettingsChanged: () {
+ _loadMapType();
+ },
+ ),
];
return Scaffold(
@@ -399,6 +417,10 @@ class _MainScreenState extends State with WidgetsBindingObserver {
if (_currentIndex == 2 && index == 0) {
_historyScreenKey.currentState?.reloadRecords();
}
+ // 如果从设置页面切换到地图页面,重新加载地图类型
+ if (_currentIndex == 2 && index == 1) {
+ _loadMapType();
+ }
setState(() {
if (_isHistoryEditMode) _isHistoryEditMode = false;
_currentIndex = index;
diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart
index 80b3087..f9cf471 100644
--- a/lib/screens/map_screen.dart
+++ b/lib/screens/map_screen.dart
@@ -22,7 +22,7 @@ class _MapScreenState extends State {
LatLng? _currentLocation;
LatLng? _lastTrainLocation;
LatLng? _userLocation;
- double _currentZoom = 12.0;
+ double _currentZoom = 14.0;
double _currentRotation = 0.0;
bool _isMapInitialized = false;
@@ -51,10 +51,19 @@ class _MapScreenState extends State {
_loadSettings().then((_) {
_loadTrainRecords().then((_) {
_startLocationUpdates();
+ if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
+ _initializeMapPosition();
+ }
});
});
}
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _loadSettings();
+ }
+
Future _checkDatabaseSettings() async {
try {
final dbInfo = await DatabaseService.instance.getDatabaseInfo();
@@ -227,6 +236,8 @@ class _MapScreenState extends State {
settings['mapCenterLon'] = center.longitude;
}
+ settings['mapSettingsTimestamp'] = DateTime.now().millisecondsSinceEpoch;
+
await DatabaseService.instance.updateSettings(settings);
} catch (e) {}
}
@@ -734,11 +745,31 @@ class _MapScreenState extends State {
);
}
+ final bool isDefaultLocation = _currentLocation == null &&
+ _lastTrainLocation == null &&
+ _userLocation == null;
+
return Scaffold(
backgroundColor: const Color(0xFF121212),
body: Stack(
children: [
- FlutterMap(
+ if (isDefaultLocation)
+ const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)),
+ ),
+ SizedBox(height: 16),
+ Text(
+ '正在加载地图位置...',
+ style: TextStyle(color: Colors.white, fontSize: 16),
+ ),
+ ],
+ ),
+ )
+ else FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _currentLocation ??
@@ -747,7 +778,7 @@ class _MapScreenState extends State {
const LatLng(39.9042, 116.4074),
initialZoom: _currentZoom,
initialRotation: _currentRotation,
- minZoom: 4.0,
+ minZoom: 8.0,
maxZoom: 18.0,
onPositionChanged: (MapCamera camera, bool hasGesture) {
setState(() {
diff --git a/lib/screens/map_webview_screen.dart b/lib/screens/map_webview_screen.dart
new file mode 100644
index 0000000..45c6778
--- /dev/null
+++ b/lib/screens/map_webview_screen.dart
@@ -0,0 +1,1162 @@
+import 'dart:convert';
+import 'dart:math' as math;
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:geolocator/geolocator.dart';
+import 'dart:async';
+import '../models/train_record.dart';
+import 'package:lbjconsole/services/database_service.dart';
+import 'package:latlong2/latlong.dart';
+
+class MapWebViewScreen extends StatefulWidget {
+ const MapWebViewScreen({super.key});
+
+ @override
+ State createState() => MapWebViewScreenState();
+}
+
+class MapWebViewScreenState extends State
+ with WidgetsBindingObserver {
+ late final WebViewController _controller;
+ Position? _currentPosition;
+ List _trainRecords = [];
+ bool _isRailwayLayerVisible = true;
+ bool _isLoading = true;
+ String _timeFilter = 'unlimited';
+ Timer? _refreshTimer;
+ Timer? _locationUpdateTimer;
+
+ bool _isMapInitialized = false;
+ bool _isLocationPermissionGranted = false;
+ double _currentZoom = 14.0;
+ double _currentRotation = 0.0;
+ LatLng? _currentLocation;
+ LatLng? _lastTrainLocation;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ _initializeWebView();
+ _initializeServices();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed) {
+ _reloadSettingsIfNeeded();
+ }
+ }
+
+ Future _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 _initializeLocation() async {
+ try {
+ bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
+ if (!serviceEnabled) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('请开启定位服务')),
+ );
+ }
+ return;
+ }
+
+ LocationPermission permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ }
+
+ if (permission == LocationPermission.whileInUse ||
+ permission == LocationPermission.always) {
+ Position position = await Geolocator.getCurrentPosition(
+ locationSettings: const LocationSettings(
+ accuracy: LocationAccuracy.best,
+ timeLimit: Duration(seconds: 15),
+ ),
+ );
+
+ setState(() {
+ _currentPosition = position;
+ _isLocationPermissionGranted = true;
+ });
+
+ _updateUserLocation();
+
+ _startLocationUpdates();
+ } else if (permission == LocationPermission.deniedForever) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')),
+ );
+ }
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')),
+ );
+ }
+ }
+ } catch (e) {}
+ }
+
+ void _startLocationUpdates() {
+ _locationUpdateTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
+ if (_isLocationPermissionGranted && mounted) {
+ _updateCurrentLocation();
+ } else {}
+ });
+ }
+
+ Future _updateCurrentLocation() async {
+ try {
+ bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
+ if (!serviceEnabled) {
+ return;
+ }
+
+ Position position = await Geolocator.getCurrentPosition(
+ desiredAccuracy: LocationAccuracy.best,
+ forceAndroidLocationManager: true,
+ );
+
+ setState(() {
+ _currentPosition = position;
+ });
+
+ _updateUserLocation();
+ } catch (e) {
+ if (e.toString().contains('PERMISSION_DENIED') ||
+ e.toString().contains('permission')) {
+ _checkAndRequestPermissions();
+ }
+ }
+ }
+
+ Future _checkAndRequestPermissions() async {
+ try {
+ LocationPermission permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ }
+
+ if (permission == LocationPermission.whileInUse ||
+ permission == LocationPermission.always) {
+ setState(() {
+ _isLocationPermissionGranted = true;
+ });
+ } else {
+ setState(() {
+ _isLocationPermissionGranted = false;
+ });
+ }
+ } catch (e) {}
+ }
+
+ Future _loadTrainRecordsFromDatabase() async {
+ try {
+ final isConnected = await DatabaseService.instance.isDatabaseConnected();
+ if (!isConnected) {}
+
+ List records;
+
+ if (_timeFilter == 'unlimited') {
+ records = await DatabaseService.instance.getAllRecords();
+ } else {
+ final duration = _getTimeFilterDuration(_timeFilter);
+ if (duration != null && duration != Duration.zero) {
+ records = await DatabaseService.instance
+ .getRecordsWithinReceivedTimeRange(duration);
+ } else {
+ records = await DatabaseService.instance.getAllRecords();
+ }
+ }
+
+ if (records.isNotEmpty) {
+ for (int i = 0; i < math.min(3, records.length); i++) {
+ final record = records[i];
+ final coords = record.getCoordinates();
+ }
+ }
+
+ setState(() {
+ _trainRecords = records;
+ _isLoading = false;
+
+ if (_trainRecords.isNotEmpty) {
+ final validRecords = [
+ ..._getValidRecords(),
+ ..._getValidDmsRecords()
+ ];
+ if (validRecords.isNotEmpty) {
+ final lastRecord = validRecords.first;
+ LatLng? position;
+
+ final dmsPosition = _parseDmsCoordinate(lastRecord.positionInfo);
+ if (dmsPosition != null) {
+ position = dmsPosition;
+ } else {
+ final coords = lastRecord.getCoordinates();
+ if (coords['lat'] != 0.0 && coords['lng'] != 0.0) {
+ position = LatLng(coords['lat']!, coords['lng']!);
+ }
+ }
+
+ if (position != null) {
+ _lastTrainLocation = position;
+ } else {}
+ } else {}
+ } else {}
+
+ 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 _loadSettings() async {
+ try {
+ final settings = await DatabaseService.instance.getAllSettings();
+ if (settings != null) {
+ setState(() {
+ _isRailwayLayerVisible =
+ (settings['mapRailwayLayerVisible'] as int?) == 1;
+ _currentZoom = (settings['mapZoomLevel'] as num?)?.toDouble() ?? 10.0;
+ _currentRotation =
+ (settings['mapRotation'] as num?)?.toDouble() ?? 0.0;
+ _timeFilter = settings['mapTimeFilter'] as String? ?? 'unlimited';
+
+ final lat = (settings['mapCenterLat'] as num?)?.toDouble();
+ final lon = (settings['mapCenterLon'] as num?)?.toDouble();
+
+ if (lat != null && lon != null && lat != 0.0 && lon != 0.0) {
+ _currentLocation = LatLng(lat, lon);
+ }
+ });
+ }
+ } catch (e) {}
+ }
+
+ Future _saveSettings() async {
+ try {
+ final settings = {
+ 'mapRailwayLayerVisible': _isRailwayLayerVisible ? 1 : 0,
+ 'mapZoomLevel': _currentZoom,
+ 'mapRotation': _currentRotation,
+ 'mapTimeFilter': _timeFilter,
+ 'mapSettingsTimestamp': DateTime.now().millisecondsSinceEpoch,
+ };
+
+ if (_currentLocation != null) {
+ settings['mapCenterLat'] = _currentLocation!.latitude;
+ settings['mapCenterLon'] = _currentLocation!.longitude;
+ }
+
+ await DatabaseService.instance.updateSettings(settings);
+ } catch (e) {}
+ }
+
+ Future _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 == '') {
+ return null;
+ }
+
+ try {
+ final parts = positionInfo.trim().split(' ');
+ if (parts.length >= 2) {
+ final latStr = parts[0];
+ final lngStr = parts[1];
+
+ final lat = _parseDmsString(latStr);
+ final lng = _parseDmsString(lngStr);
+
+ if (lat != null &&
+ lng != null &&
+ (lat.abs() > 0.001 || lng.abs() > 0.001)) {
+ return LatLng(lat, lng);
+ }
+ }
+ } catch (e) {}
+
+ return null;
+ }
+
+ double? _parseDmsString(String dmsStr) {
+ try {
+ final degreeIndex = dmsStr.indexOf('°');
+ if (degreeIndex == -1) return null;
+
+ final degrees = double.tryParse(dmsStr.substring(0, degreeIndex));
+ if (degrees == null) return null;
+
+ final minuteIndex = dmsStr.indexOf('′');
+ if (minuteIndex == -1) return degrees;
+
+ final minutes =
+ double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex));
+ if (minutes == null) return degrees;
+
+ return degrees + (minutes / 60.0);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ List _getValidRecords() {
+ return _trainRecords.where((record) {
+ final coords = record.getCoordinates();
+ return coords['lat'] != 0.0 && coords['lng'] != 0.0;
+ }).toList();
+ }
+
+ List _getValidDmsRecords() {
+ return _trainRecords.where((record) {
+ return _parseDmsCoordinate(record.positionInfo) != null;
+ }).toList();
+ }
+
+ void _updateTrainMarkers() {
+ if (_trainRecords.isEmpty) {
+ return;
+ }
+
+ if (!_isMapInitialized) {
+ Future.delayed(const Duration(seconds: 2), () {
+ if (mounted && _trainRecords.isNotEmpty) {
+ _updateTrainMarkers();
+ }
+ });
+ return;
+ }
+
+ final validRecords = [..._getValidRecords(), ..._getValidDmsRecords()];
+
+ final trainData = validRecords
+ .map((record) {
+ LatLng? position;
+
+ final dmsPosition = _parseDmsCoordinate(record.positionInfo);
+ if (dmsPosition != null) {
+ position = dmsPosition;
+ } else {
+ final coords = record.getCoordinates();
+ if (coords['lat'] != 0.0 && coords['lng'] != 0.0) {
+ position = LatLng(coords['lat']!, coords['lng']!);
+ }
+ }
+
+ if (position != null) {
+ return {
+ 'lat': position.latitude,
+ 'lng': position.longitude,
+ 'fullTrainNumber': record.fullTrainNumber,
+ 'time': record.time,
+ 'speed': record.speed,
+ 'position': record.position,
+ 'route': record.route,
+ 'locoType': record.locoType,
+ 'loco': record.loco,
+ 'timestamp': record.timestamp,
+ };
+ }
+ return null;
+ })
+ .where((data) => data != null)
+ .toList();
+
+ trainData.sort((a, b) {
+ final aTimestamp = a?['timestamp'];
+ final bTimestamp = b?['timestamp'];
+
+ if (aTimestamp == null || bTimestamp == null) return 0;
+
+ if (aTimestamp is DateTime && bTimestamp is DateTime) {
+ return bTimestamp.compareTo(aTimestamp);
+ }
+
+ if (aTimestamp is int && bTimestamp is int) {
+ return bTimestamp.compareTo(aTimestamp);
+ }
+
+ if (aTimestamp is String && bTimestamp is String) {
+ return bTimestamp.compareTo(aTimestamp);
+ }
+
+ return 0;
+ });
+
+ final jsonTrainData = trainData
+ .map((train) {
+ if (train == null) return null;
+
+ final trainCopy = Map.from(train);
+
+ if (trainCopy['timestamp'] is DateTime) {
+ trainCopy['timestamp'] =
+ (trainCopy['timestamp'] as DateTime).millisecondsSinceEpoch;
+ }
+
+ return trainCopy;
+ })
+ .where((train) => train != null)
+ .toList();
+
+ if (trainData.isEmpty) {
+ return;
+ }
+
+ if (trainData.isNotEmpty) {
+ final latestTrain = trainData.first;
+ if (latestTrain != null) {
+ _lastTrainLocation = LatLng((latestTrain['lat'] as num).toDouble(),
+ (latestTrain['lng'] as num).toDouble());
+ }
+ }
+
+ _controller.runJavaScript('''
+ (function() {
+ try {
+ if (window.MapInterface && window.MapInterface.updateTrainMarkers) {
+ window.MapInterface.updateTrainMarkers(${jsonEncode(jsonTrainData)});
+
+ if (!window.trainMarkersUpdated && ${trainData.length} > 0) {
+ window.trainMarkersUpdated = true;
+ setTimeout(function() {
+ if (window.MapInterface && window.MapInterface.fitBounds) {
+ window.MapInterface.fitBounds();
+ }
+ }, 1000);
+ }
+ } else {
+ console.error('MapInterface 未定义或updateTrainMarkers方法不存在');
+ }
+ } catch (error) {
+ console.error('更新列车标记失败:', error);
+ }
+ })();
+ ''').then((result) {}).catchError((error) {});
+ }
+
+ void _updateUserLocation() {
+ if (_currentPosition != null) {
+ _controller.runJavaScript('''
+ (function() {
+ try {
+ if (window.MapInterface && window.MapInterface.setUserLocation) {
+ window.MapInterface.setUserLocation(${_currentPosition!.latitude}, ${_currentPosition!.longitude});
+
+ if (!window.userLocationUpdated) {
+ window.userLocationUpdated = true;
+ }
+ } else {
+ console.error('MapInterface 未定义或setUserLocation方法不存在');
+ }
+ } catch (error) {
+ console.error('更新用户位置失败:', error);
+ }
+ })();
+ ''').then((_) {}).catchError((error) {});
+ } else {}
+ }
+
+ void _toggleRailwayLayer() {
+ setState(() {
+ _isRailwayLayerVisible = !_isRailwayLayerVisible;
+ });
+
+ _controller.runJavaScript('''
+ (function() {
+ if (window.MapInterface) {
+ try {
+ window.MapInterface.setRailwayVisible($_isRailwayLayerVisible);
+ console.log('铁路图层切换成功: $_isRailwayLayerVisible');
+ } catch (error) {
+ console.error('切换铁路图层失败:', error);
+ }
+ } else {
+ console.error('MapInterface 未定义');
+ }
+ })();
+ ''').then((result) {}).catchError((error) {});
+ }
+
+ void _showTrainDetailsDialog(Map train) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ backgroundColor: const Color(0xFF1E1E1E),
+ title: Text(
+ train['trainNumber'] ?? '未知车次',
+ style: const TextStyle(color: Colors.white),
+ ),
+ content: SingleChildScrollView(
+ child: ListBody(
+ children: [
+ _buildDetailRow('车次', train['trainNumber'] ?? '未知'),
+ _buildDetailRow('类型', train['trainType'] ?? '未知'),
+ _buildDetailRow('速度', '${train['speed'] ?? 0} km/h'),
+ _buildDetailRow('方向', '${train['direction'] ?? 0}°'),
+ _buildDetailRow(
+ '经度', train['longitude']?.toStringAsFixed(6) ?? '未知'),
+ _buildDetailRow(
+ '纬度', train['latitude']?.toStringAsFixed(6) ?? '未知'),
+ _buildDetailRow('时间', _getDisplayTime(train['timestamp'])),
+ _buildDetailRow('日期', _getDisplayDate(train['timestamp'])),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('关闭', style: TextStyle(color: Colors.white)),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ _controller.runJavaScript('''
+ if (window.MapInterface) {
+ window.MapInterface.setCenter(${train['latitude']}, ${train['longitude']}, 16);
+ }
+ ''');
+ },
+ child:
+ const Text('定位', style: TextStyle(color: Color(0xFF007ACC))),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildDetailRow(String label, String value) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 80,
+ child: Text(
+ '$label:',
+ style: const TextStyle(
+ color: Colors.grey,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Text(
+ value,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ String _getDisplayTime(int? timestamp) {
+ if (timestamp == null) return '未知';
+ final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
+ return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}';
+ }
+
+ String _getDisplayDate(int? timestamp) {
+ if (timestamp == null) return '未知';
+ final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
+ return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}';
+ }
+
+ void _showTimeFilterDialog() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return SimpleDialog(
+ title: const Text('时间筛选'),
+ children: [
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = 'unlimited';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('无限制'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '1hour';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近1小时'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '6hours';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近6小时'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '12hours';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近12小时'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '24hours';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近24小时'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '7days';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近7天'),
+ ),
+ SimpleDialogOption(
+ onPressed: () {
+ setState(() {
+ _timeFilter = '30days';
+ });
+ Navigator.of(context).pop();
+ _saveSettingsAndReload();
+ },
+ child: const Text('最近30天'),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ void _saveSettingsAndReload() async {
+ try {
+ await _saveSettings();
+ _loadTrainRecordsFromDatabase();
+ } catch (e) {}
+ }
+
+ String _getTimeFilterLabel() {
+ switch (_timeFilter) {
+ case '1hour':
+ return '1小时';
+ case '6hours':
+ return '6小时';
+ case '12hours':
+ return '12小时';
+ case '24hours':
+ return '24小时';
+ case '7days':
+ return '7天';
+ case '30days':
+ return '30天';
+ default:
+ return '无限制';
+ }
+ }
+
+ void _onMapStateChanged(Map mapState) {
+ try {
+ final lat = mapState['lat'] as 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 _forceUpdateLocation() async {
+ try {
+ bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
+ if (!serviceEnabled) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('请开启定位服务')),
+ );
+ }
+ return;
+ }
+
+ LocationPermission permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ }
+
+ if (permission == LocationPermission.deniedForever) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('定位权限被永久拒绝,请在设置中开启')),
+ );
+ }
+ return;
+ }
+
+ if (permission != LocationPermission.whileInUse &&
+ permission != LocationPermission.always) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('定位权限不足')),
+ );
+ }
+ return;
+ }
+
+ Position position = await Geolocator.getCurrentPosition(
+ desiredAccuracy: LocationAccuracy.best,
+ forceAndroidLocationManager: true,
+ timeLimit: const Duration(seconds: 20),
+ );
+
+ final newLocation = LatLng(position.latitude, position.longitude);
+ setState(() {
+ _currentPosition = position;
+ _isLocationPermissionGranted = true;
+ });
+
+ _centerMap(newLocation, zoom: 16.0);
+
+ _updateUserLocation();
+ } catch (e) {}
+ }
+
+ void _refreshMap() {
+ _loadTrainRecordsFromDatabase();
+
+ if (_isLocationPermissionGranted) {
+ _forceUpdateLocation();
+ } else {
+ _updateUserLocation();
+ }
+
+ _controller.runJavaScript('''
+ if (window.MapInterface) {
+ window.MapInterface.getTrainMarkersCount();
+ }
+ ''');
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ _saveSettings();
+
+ _refreshTimer?.cancel();
+ _locationUpdateTimer?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFF121212),
+ body: Stack(
+ children: [
+ WebViewWidget(controller: _controller),
+ if (_isLoading)
+ Container(
+ color: const Color(0xFF121212).withValues(alpha: 0.8),
+ child: const Center(
+ child: CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)),
+ ),
+ ),
+ ),
+ Positioned(
+ right: 16,
+ bottom: 80,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ FloatingActionButton(
+ heroTag: "time_filter",
+ mini: true,
+ backgroundColor: const Color(0xFF1E1E1E),
+ onPressed: () {
+ _reloadSettingsIfNeeded();
+ _showTimeFilterDialog();
+ },
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.filter_list,
+ color: Colors.white, size: 18),
+ Text(
+ _getTimeFilterLabel(),
+ style:
+ const TextStyle(color: Colors.white, fontSize: 8),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
+ FloatingActionButton(
+ heroTag: "refresh",
+ mini: true,
+ backgroundColor: const Color(0xFF1E1E1E),
+ onPressed: _refreshMap,
+ child: const Icon(Icons.refresh, color: Colors.white),
+ ),
+ const SizedBox(height: 8),
+ FloatingActionButton(
+ heroTag: "layers",
+ mini: true,
+ backgroundColor: const Color(0xFF1E1E1E),
+ onPressed: _toggleRailwayLayer,
+ child: Icon(
+ _isRailwayLayerVisible
+ ? Icons.layers
+ : Icons.layers_outlined,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(height: 8),
+ FloatingActionButton(
+ heroTag: "location",
+ mini: true,
+ backgroundColor: const Color(0xFF1E1E1E),
+ onPressed: _centerToUserLocation,
+ child: const Icon(Icons.my_location, color: Colors.white),
+ ),
+ const SizedBox(height: 8),
+ FloatingActionButton(
+ heroTag: "last_train",
+ mini: true,
+ backgroundColor: const Color(0xFF1E1E1E),
+ onPressed: _centerToLastTrain,
+ child: const Icon(Icons.train, color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
index e542207..5645f48 100644
--- a/lib/screens/settings_screen.dart
+++ b/lib/screens/settings_screen.dart
@@ -17,7 +17,9 @@ import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';
class SettingsScreen extends StatefulWidget {
- const SettingsScreen({super.key});
+ final VoidCallback? onSettingsChanged;
+
+ const SettingsScreen({super.key, this.onSettingsChanged});
@override
State createState() => _SettingsScreenState();
@@ -33,8 +35,10 @@ class _SettingsScreenState extends State {
int _recordCount = 0;
bool _mergeRecordsEnabled = false;
bool _hideTimeOnlyRecords = false;
+ bool _hideUngroupableRecords = false;
GroupBy _groupBy = GroupBy.trainAndLoco;
TimeWindow _timeWindow = TimeWindow.unlimited;
+ String _mapType = 'map';
@override
void initState() {
@@ -63,8 +67,10 @@ class _SettingsScreenState extends State {
_notificationsEnabled = (settingsMap['notificationEnabled'] ?? 1) == 1;
_mergeRecordsEnabled = settings.enabled;
_hideTimeOnlyRecords = (settingsMap['hideTimeOnlyRecords'] ?? 0) == 1;
+ _hideUngroupableRecords = settings.hideUngroupableRecords;
_groupBy = settings.groupBy;
_timeWindow = settings.timeWindow;
+ _mapType = settingsMap['mapType']?.toString() ?? 'webview';
});
}
}
@@ -85,9 +91,12 @@ class _SettingsScreenState extends State {
'notificationEnabled': _notificationsEnabled ? 1 : 0,
'mergeRecordsEnabled': _mergeRecordsEnabled ? 1 : 0,
'hideTimeOnlyRecords': _hideTimeOnlyRecords ? 1 : 0,
+ 'hideUngroupableRecords': _hideUngroupableRecords ? 1 : 0,
'groupBy': _groupBy.name,
'timeWindow': _timeWindow.name,
+ 'mapType': _mapType,
});
+ widget.onSettingsChanged?.call();
}
@override
@@ -240,6 +249,43 @@ class _SettingsScreenState extends State {
],
),
const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('地图显示方式', style: AppTheme.bodyLarge),
+ Text('选择地图组件类型', style: AppTheme.caption),
+ ],
+ ),
+ DropdownButton(
+ value: _mapType,
+ items: [
+ DropdownMenuItem(
+ value: 'webview',
+ child: Text('矢量铁路地图', style: AppTheme.bodyMedium),
+ ),
+ DropdownMenuItem(
+ value: 'map',
+ child: Text('栅格铁路地图', style: AppTheme.bodyMedium),
+ ),
+ ],
+ onChanged: (value) {
+ if (value != null) {
+ setState(() {
+ _mapType = value;
+ });
+ _saveSettings();
+ }
+ },
+ dropdownColor: AppTheme.secondaryBlack,
+ style: AppTheme.bodyMedium,
+ underline: Container(height: 0),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -398,6 +444,29 @@ class _SettingsScreenState extends State {
dropdownColor: AppTheme.secondaryBlack,
style: AppTheme.bodyMedium,
),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
+ Text('不显示无法分组的记录', style: AppTheme.caption),
+ ],
+ ),
+ Switch(
+ value: _hideUngroupableRecords,
+ onChanged: (value) {
+ setState(() {
+ _hideUngroupableRecords = value;
+ });
+ _saveSettings();
+ },
+ activeColor: Theme.of(context).colorScheme.primary,
+ ),
+ ],
+ ),
],
),
),
@@ -421,7 +490,8 @@ class _SettingsScreenState extends State {
children: [
Row(
children: [
- Icon(Icons.storage, color: Theme.of(context).colorScheme.primary),
+ Icon(Icons.storage,
+ color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text('数据管理', style: AppTheme.titleMedium),
],
diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart
index d9a3a16..482212e 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 = 4;
+ static const _databaseVersion = 6;
static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings';
@@ -21,21 +21,47 @@ class DatabaseService {
Database? _database;
Future get database async {
- if (_database != null) return _database!;
- _database = await _initDatabase();
- return _database!;
+ try {
+ if (_database != null) {
+ return _database!;
+ }
+ _database = await _initDatabase();
+ return _database!;
+ } catch (e, stackTrace) {
+ rethrow;
+ }
+ }
+
+ Future isDatabaseConnected() async {
+ try {
+ if (_database == null) {
+ return false;
+ }
+
+ final db = await database;
+ final result = await db.rawQuery('SELECT 1');
+ return true;
+ } catch (e) {
+ return false;
+ }
}
Future _initDatabase() async {
- final directory = await getApplicationDocumentsDirectory();
- final path = join(directory.path, _databaseName);
+ try {
+ final directory = await getApplicationDocumentsDirectory();
+ final path = join(directory.path, _databaseName);
- return await openDatabase(
- path,
- version: _databaseVersion,
- onCreate: _onCreate,
- onUpgrade: _onUpgrade,
- );
+ final db = await openDatabase(
+ path,
+ version: _databaseVersion,
+ onCreate: _onCreate,
+ onUpgrade: _onUpgrade,
+ );
+
+ return db;
+ } catch (e, stackTrace) {
+ rethrow;
+ }
}
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
@@ -51,8 +77,15 @@ class DatabaseService {
try {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapTimeFilter TEXT NOT NULL DEFAULT "unlimited"');
- } catch (e) {
- }
+ } catch (e) {}
+ }
+ if (oldVersion < 5) {
+ await db.execute(
+ 'ALTER TABLE $appSettingsTable ADD COLUMN mapType TEXT NOT NULL DEFAULT "webview"');
+ }
+ if (oldVersion < 6) {
+ await db.execute(
+ 'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0');
}
}
@@ -92,6 +125,7 @@ class DatabaseService {
mapZoomLevel REAL NOT NULL DEFAULT 10.0,
mapRailwayLayerVisible INTEGER NOT NULL DEFAULT 1,
mapRotation REAL NOT NULL DEFAULT 0.0,
+ mapType TEXT NOT NULL DEFAULT 'webview',
specifiedDeviceAddress TEXT,
searchOrderList TEXT NOT NULL DEFAULT '',
autoConnectEnabled INTEGER NOT NULL DEFAULT 1,
@@ -101,7 +135,8 @@ class DatabaseService {
hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0,
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
timeWindow TEXT NOT NULL DEFAULT 'unlimited',
- mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited'
+ mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
+ hideUngroupableRecords INTEGER NOT NULL DEFAULT 0
)
''');
@@ -118,6 +153,7 @@ class DatabaseService {
'mapZoomLevel': 10.0,
'mapRailwayLayerVisible': 1,
'mapRotation': 0.0,
+ 'mapType': 'webview',
'searchOrderList': '',
'autoConnectEnabled': 1,
'backgroundServiceEnabled': 0,
@@ -127,6 +163,7 @@ class DatabaseService {
'groupBy': 'trainAndLoco',
'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited',
+ 'hideUngroupableRecords': 0,
});
}
@@ -140,12 +177,18 @@ class DatabaseService {
}
Future> getAllRecords() async {
- final db = await database;
- final result = await db.query(
- trainRecordsTable,
- orderBy: 'timestamp DESC',
- );
- return result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
+ try {
+ final db = await database;
+ final result = await db.query(
+ trainRecordsTable,
+ orderBy: 'timestamp DESC',
+ );
+ final records =
+ result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
+ return records;
+ } catch (e, stackTrace) {
+ rethrow;
+ }
}
Future> getRecordsWithinTimeRange(Duration duration) async {
@@ -162,15 +205,23 @@ class DatabaseService {
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();
+ try {
+ final db = await database;
+ final cutoffTime =
+ DateTime.now().subtract(duration).millisecondsSinceEpoch;
+
+ final result = await db.query(
+ trainRecordsTable,
+ where: 'receivedTimestamp >= ?',
+ whereArgs: [cutoffTime],
+ orderBy: 'receivedTimestamp DESC',
+ );
+ final records =
+ result.map((json) => TrainRecord.fromDatabaseJson(json)).toList();
+ return records;
+ } catch (e, stackTrace) {
+ rethrow;
+ }
}
Future deleteRecord(String uniqueId) async {
diff --git a/lib/services/merge_service.dart b/lib/services/merge_service.dart
index a2264e6..2be92de 100644
--- a/lib/services/merge_service.dart
+++ b/lib/services/merge_service.dart
@@ -2,6 +2,37 @@ import 'package:lbjconsole/models/train_record.dart';
import 'package:lbjconsole/models/merged_record.dart';
class MergeService {
+ static bool isNeverGroupableRecord(TrainRecord record, GroupBy groupBy) {
+ final train = record.train.trim();
+ final loco = record.loco.trim();
+
+ final hasValidTrain =
+ train.isNotEmpty && train != "" && !train.contains("-----");
+ final hasValidLoco = loco.isNotEmpty && loco != "";
+
+ switch (groupBy) {
+ case GroupBy.trainOnly:
+ return !hasValidTrain;
+
+ case GroupBy.locoOnly:
+ return !hasValidLoco;
+
+ case GroupBy.trainAndLoco:
+ return !hasValidTrain || !hasValidLoco;
+
+ case GroupBy.trainOrLoco:
+ return !hasValidTrain && !hasValidLoco;
+ }
+ }
+
+ static List filterUngroupableRecords(
+ List records, GroupBy groupBy, bool hideUngroupable) {
+ if (!hideUngroupable) return records;
+ return records
+ .where((record) => !isNeverGroupableRecord(record, groupBy))
+ .toList();
+ }
+
static String? _generateGroupKey(TrainRecord record, GroupBy groupBy) {
final train = record.train.trim();
final loco = record.loco.trim();
@@ -36,15 +67,19 @@ class MergeService {
return allRecords;
}
- allRecords
+ final filteredRecords = filterUngroupableRecords(
+ allRecords, settings.groupBy, settings.hideUngroupableRecords);
+
+ filteredRecords
.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp));
if (settings.groupBy == GroupBy.trainOrLoco) {
- return _groupByTrainOrLocoWithTimeWindow(allRecords, settings.timeWindow);
+ return _groupByTrainOrLocoWithTimeWindow(
+ filteredRecords, settings.timeWindow);
}
final groupedRecords = >{};
- for (final record in allRecords) {
+ for (final record in filteredRecords) {
final key = _generateGroupKey(record, settings.groupBy);
if (key != null) {
groupedRecords.putIfAbsent(key, () => []).add(record);
@@ -79,8 +114,9 @@ class MergeService {
final reusedRecords = _reuseDiscardedRecords(
discardedRecords, mergedRecordIds, settings.groupBy);
- final singleRecords =
- allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList();
+ final singleRecords = filteredRecords
+ .where((r) => !mergedRecordIds.contains(r.uniqueId))
+ .toList();
final List