From 06aa8491b488a7ef050b3d4a696d871caae5b45f Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Thu, 16 Oct 2025 15:47:22 +0800 Subject: [PATCH] feat: add user location display and improve route display --- lib/screens/map_screen.dart | 83 ++++++------ lib/screens/realtime_screen.dart | 211 +++++++++++++++++++++++++------ pubspec.yaml | 2 +- 3 files changed, 215 insertions(+), 81 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f9cf471..3460e74 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -51,7 +51,10 @@ class _MapScreenState extends State { _loadSettings().then((_) { _loadTrainRecords().then((_) { _startLocationUpdates(); - if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) { + if (!_isMapInitialized && + (_currentLocation != null || + _lastTrainLocation != null || + _userLocation != null)) { _initializeMapPosition(); } }); @@ -418,8 +421,6 @@ class _MapScreenState extends State { fontSize: 8, fontWeight: FontWeight.bold, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, ), ), ], @@ -745,9 +746,9 @@ class _MapScreenState extends State { ); } - final bool isDefaultLocation = _currentLocation == null && - _lastTrainLocation == null && - _userLocation == null; + final bool isDefaultLocation = _currentLocation == null && + _lastTrainLocation == null && + _userLocation == null; return Scaffold( backgroundColor: const Color(0xFF121212), @@ -759,7 +760,8 @@ class _MapScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF007ACC)), + valueColor: + AlwaysStoppedAnimation(Color(0xFF007ACC)), ), SizedBox(height: 16), Text( @@ -769,44 +771,45 @@ class _MapScreenState extends State { ], ), ) - else FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: _currentLocation ?? - _lastTrainLocation ?? - _userLocation ?? - const LatLng(39.9042, 116.4074), - initialZoom: _currentZoom, - initialRotation: _currentRotation, - minZoom: 8.0, - maxZoom: 18.0, - onPositionChanged: (MapCamera camera, bool hasGesture) { - setState(() { - _currentLocation = camera.center; - _currentZoom = camera.zoom; - _currentRotation = camera.rotation; - }); + else + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _currentLocation ?? + _lastTrainLocation ?? + _userLocation ?? + const LatLng(39.9042, 116.4074), + initialZoom: _currentZoom, + initialRotation: _currentRotation, + minZoom: 2.0, + maxZoom: 18.0, + onPositionChanged: (MapCamera camera, bool hasGesture) { + setState(() { + _currentLocation = camera.center; + _currentZoom = camera.zoom; + _currentRotation = camera.rotation; + }); - _saveSettings(); - }, - ), - children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'org.noxylva.lbjconsole', + _saveSettings(); + }, ), - if (_railwayLayerVisible) + children: [ TileLayer( - urlTemplate: - 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'org.noxylva.lbjconsole', ), - MarkerLayer( - markers: markers, - ), - ], - ), + if (_railwayLayerVisible) + TileLayer( + urlTemplate: + 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', + subdomains: const ['a', 'b', 'c'], + userAgentPackageName: 'org.noxylva.lbjconsole.flutter', + ), + MarkerLayer( + markers: markers, + ), + ], + ), if (_isLoading) const Center( child: CircularProgressIndicator( diff --git a/lib/screens/realtime_screen.dart b/lib/screens/realtime_screen.dart index 5c361b9..41dbfed 100644 --- a/lib/screens/realtime_screen.dart +++ b/lib/screens/realtime_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:geolocator/geolocator.dart'; import '../models/merged_record.dart'; import '../services/database_service.dart'; import '../models/train_record.dart'; @@ -30,6 +31,10 @@ class RealtimeScreenState extends State { List _mapMarkers = []; bool _showMap = true; Set _selectedGroupKeys = {}; + LatLng? _userLocation; + bool _isLocationPermissionGranted = false; + Timer? _locationTimer; + StreamSubscription? _positionStreamSubscription; List getDisplayItems() => _displayItems; @@ -85,6 +90,28 @@ class RealtimeScreenState extends State { .where((marker) => marker != null) .cast() .toList(); + + if (_userLocation != null) { + _mapMarkers.add( + Marker( + point: _userLocation!, + width: 24, + height: 24, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white, width: 1), + ), + child: const Icon( + Icons.my_location, + color: Colors.white, + size: 12, + ), + ), + ), + ); + } }); } @@ -166,19 +193,29 @@ class RealtimeScreenState extends State { markers: [ Marker( point: position, - width: 60, - height: 20, - child: Container( - color: Colors.black, - alignment: Alignment.center, - child: Text( - _getTrainDisplayName(singleRecord), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + width: 80, + height: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + _getTrainDisplayName(singleRecord), + style: const TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ), ), - ), + ], ), ), ], @@ -204,19 +241,29 @@ class RealtimeScreenState extends State { markers: [ Marker( point: routePoints.last, - width: 60, - height: 20, - child: Container( - color: Colors.black, - alignment: Alignment.center, - child: Text( - _getTrainDisplayName(mergedRecord.latestRecord), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + width: 80, + height: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + _getTrainDisplayName(mergedRecord.latestRecord), + style: const TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ), ), - ), + ], ), ), ], @@ -485,6 +532,7 @@ class RealtimeScreenState extends State { }); _setupRecordDeleteListener(); _setupSettingsListener(); + _startLocationUpdates(); } void _scheduleInitialScroll() { @@ -507,6 +555,8 @@ class RealtimeScreenState extends State { _scrollController.dispose(); _recordDeleteSubscription?.cancel(); _settingsSubscription?.cancel(); + _locationTimer?.cancel(); + _positionStreamSubscription?.cancel(); super.dispose(); } @@ -528,6 +578,70 @@ class RealtimeScreenState extends State { }); } + 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(); + _startRealtimeLocationUpdates(); + } + + Future _getCurrentLocation() async { + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + forceAndroidLocationManager: true, + ); + + final newLocation = LatLng(position.latitude, position.longitude); + setState(() { + _userLocation = newLocation; + }); + + _updateAllRecordMarkers(); + } catch (e) {} + } + + void _startLocationUpdates() { + _requestLocationPermission(); + } + + void _startRealtimeLocationUpdates() { + _positionStreamSubscription?.cancel(); + + _positionStreamSubscription = Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 1, + timeLimit: Duration(seconds: 30), + ), + ).listen( + (Position position) { + final newLocation = LatLng(position.latitude, position.longitude); + setState(() { + _userLocation = newLocation; + }); + _updateAllRecordMarkers(); + }, + onError: (error) {}, + ); + } + Future loadRecords({bool scrollToTop = true}) async { try { if (mounted) { @@ -1169,13 +1283,23 @@ class RealtimeScreenState extends State { } final latestRoute = getValidRoute(latestRecord); - final previousRoute = - previousRecord != null ? getValidRoute(previousRecord) : ""; - final bool needsSpecialDisplay = previousRecord != null && - latestRoute.isNotEmpty && - previousRoute.isNotEmpty && - latestRoute != previousRoute; + String displayRoute = latestRoute; + bool isDisplayingLatestNormal = true; + + if (latestRoute.isEmpty || latestRoute.contains('*')) { + for (final record in mergedRecord.records) { + final route = getValidRoute(record); + if (route.isNotEmpty && !route.contains('*')) { + displayRoute = route; + isDisplayingLatestNormal = (record == latestRecord); + break; + } + } + } + + final bool needsSpecialDisplay = !isDisplayingLatestNormal || + (latestRoute.contains('*') && displayRoute != latestRoute); final position = latestRecord.position.trim(); final speed = latestRecord.speed.trim(); @@ -1203,10 +1327,10 @@ class RealtimeScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (latestRoute.isNotEmpty) ...[ + if (displayRoute.isNotEmpty) ...[ if (needsSpecialDisplay) ...[ Flexible( - child: Text(previousRoute, + child: Text(displayRoute, style: const TextStyle( fontSize: 16, color: Colors.white, @@ -1221,18 +1345,25 @@ class RealtimeScreenState extends State { context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1E1E1E), - title: const Text("路线变化", + title: const Text("路线信息", style: TextStyle(color: Colors.white)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("上一条: $previousRoute", - style: const TextStyle(color: Colors.grey)), - const SizedBox(height: 8), - Text("当前: $latestRoute", - style: - const TextStyle(color: Colors.white)), + if (!isDisplayingLatestNormal) ...[ + Text("显示路线: $displayRoute", + style: + const TextStyle(color: Colors.white)), + const SizedBox(height: 8), + ], + Text( + "最新路线: ${latestRoute.isNotEmpty ? latestRoute : '无效路线'}", + style: TextStyle( + color: latestRoute.isNotEmpty + ? Colors.grey + : Colors.red, + )), ], ), actions: [ @@ -1261,7 +1392,7 @@ class RealtimeScreenState extends State { const SizedBox(width: 4), ] else Flexible( - child: Text(latestRoute, + child: Text(displayRoute, style: const TextStyle( fontSize: 16, color: Colors.white), overflow: TextOverflow.ellipsis)), diff --git a/pubspec.yaml b/pubspec.yaml index 106a386..6e782f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.6.1-flutter+61 +version: 0.7.0-flutter+70 environment: sdk: ^3.5.4