feat: add user location display and improve route display
This commit is contained in:
@@ -51,7 +51,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_loadSettings().then((_) {
|
_loadSettings().then((_) {
|
||||||
_loadTrainRecords().then((_) {
|
_loadTrainRecords().then((_) {
|
||||||
_startLocationUpdates();
|
_startLocationUpdates();
|
||||||
if (!_isMapInitialized && (_currentLocation != null || _lastTrainLocation != null || _userLocation != null)) {
|
if (!_isMapInitialized &&
|
||||||
|
(_currentLocation != null ||
|
||||||
|
_lastTrainLocation != null ||
|
||||||
|
_userLocation != null)) {
|
||||||
_initializeMapPosition();
|
_initializeMapPosition();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -418,8 +421,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -745,9 +746,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool isDefaultLocation = _currentLocation == null &&
|
final bool isDefaultLocation = _currentLocation == null &&
|
||||||
_lastTrainLocation == null &&
|
_lastTrainLocation == null &&
|
||||||
_userLocation == null;
|
_userLocation == null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF121212),
|
backgroundColor: const Color(0xFF121212),
|
||||||
@@ -759,7 +760,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF007ACC)),
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(Color(0xFF007ACC)),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
@@ -769,44 +771,45 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else FlutterMap(
|
else
|
||||||
mapController: _mapController,
|
FlutterMap(
|
||||||
options: MapOptions(
|
mapController: _mapController,
|
||||||
initialCenter: _currentLocation ??
|
options: MapOptions(
|
||||||
_lastTrainLocation ??
|
initialCenter: _currentLocation ??
|
||||||
_userLocation ??
|
_lastTrainLocation ??
|
||||||
const LatLng(39.9042, 116.4074),
|
_userLocation ??
|
||||||
initialZoom: _currentZoom,
|
const LatLng(39.9042, 116.4074),
|
||||||
initialRotation: _currentRotation,
|
initialZoom: _currentZoom,
|
||||||
minZoom: 8.0,
|
initialRotation: _currentRotation,
|
||||||
maxZoom: 18.0,
|
minZoom: 2.0,
|
||||||
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
maxZoom: 18.0,
|
||||||
setState(() {
|
onPositionChanged: (MapCamera camera, bool hasGesture) {
|
||||||
_currentLocation = camera.center;
|
setState(() {
|
||||||
_currentZoom = camera.zoom;
|
_currentLocation = camera.center;
|
||||||
_currentRotation = camera.rotation;
|
_currentZoom = camera.zoom;
|
||||||
});
|
_currentRotation = camera.rotation;
|
||||||
|
});
|
||||||
|
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
},
|
},
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TileLayer(
|
|
||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
||||||
userAgentPackageName: 'org.noxylva.lbjconsole',
|
|
||||||
),
|
),
|
||||||
if (_railwayLayerVisible)
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate:
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
|
|
||||||
subdomains: const ['a', 'b', 'c'],
|
|
||||||
userAgentPackageName: 'org.noxylva.lbjconsole',
|
userAgentPackageName: 'org.noxylva.lbjconsole',
|
||||||
),
|
),
|
||||||
MarkerLayer(
|
if (_railwayLayerVisible)
|
||||||
markers: markers,
|
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)
|
if (_isLoading)
|
||||||
const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
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';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
import '../models/merged_record.dart';
|
import '../models/merged_record.dart';
|
||||||
import '../services/database_service.dart';
|
import '../services/database_service.dart';
|
||||||
import '../models/train_record.dart';
|
import '../models/train_record.dart';
|
||||||
@@ -30,6 +31,10 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
List<Marker> _mapMarkers = [];
|
List<Marker> _mapMarkers = [];
|
||||||
bool _showMap = true;
|
bool _showMap = true;
|
||||||
Set<String> _selectedGroupKeys = {};
|
Set<String> _selectedGroupKeys = {};
|
||||||
|
LatLng? _userLocation;
|
||||||
|
bool _isLocationPermissionGranted = false;
|
||||||
|
Timer? _locationTimer;
|
||||||
|
StreamSubscription<Position>? _positionStreamSubscription;
|
||||||
|
|
||||||
List<Object> getDisplayItems() => _displayItems;
|
List<Object> getDisplayItems() => _displayItems;
|
||||||
|
|
||||||
@@ -85,6 +90,28 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
.where((marker) => marker != null)
|
.where((marker) => marker != null)
|
||||||
.cast<Marker>()
|
.cast<Marker>()
|
||||||
.toList();
|
.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<RealtimeScreen> {
|
|||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
point: position,
|
point: position,
|
||||||
width: 60,
|
width: 80,
|
||||||
height: 20,
|
height: 16,
|
||||||
child: Container(
|
child: Column(
|
||||||
color: Colors.black,
|
mainAxisSize: MainAxisSize.min,
|
||||||
alignment: Alignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Text(
|
children: [
|
||||||
_getTrainDisplayName(singleRecord),
|
Container(
|
||||||
style: const TextStyle(
|
padding: const EdgeInsets.symmetric(
|
||||||
color: Colors.white,
|
horizontal: 6, vertical: 2),
|
||||||
fontSize: 10,
|
decoration: BoxDecoration(
|
||||||
fontWeight: FontWeight.bold,
|
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<RealtimeScreen> {
|
|||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
point: routePoints.last,
|
point: routePoints.last,
|
||||||
width: 60,
|
width: 80,
|
||||||
height: 20,
|
height: 16,
|
||||||
child: Container(
|
child: Column(
|
||||||
color: Colors.black,
|
mainAxisSize: MainAxisSize.min,
|
||||||
alignment: Alignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Text(
|
children: [
|
||||||
_getTrainDisplayName(mergedRecord.latestRecord),
|
Container(
|
||||||
style: const TextStyle(
|
padding: const EdgeInsets.symmetric(
|
||||||
color: Colors.white,
|
horizontal: 6, vertical: 2),
|
||||||
fontSize: 10,
|
decoration: BoxDecoration(
|
||||||
fontWeight: FontWeight.bold,
|
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<RealtimeScreen> {
|
|||||||
});
|
});
|
||||||
_setupRecordDeleteListener();
|
_setupRecordDeleteListener();
|
||||||
_setupSettingsListener();
|
_setupSettingsListener();
|
||||||
|
_startLocationUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleInitialScroll() {
|
void _scheduleInitialScroll() {
|
||||||
@@ -507,6 +555,8 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_recordDeleteSubscription?.cancel();
|
_recordDeleteSubscription?.cancel();
|
||||||
_settingsSubscription?.cancel();
|
_settingsSubscription?.cancel();
|
||||||
|
_locationTimer?.cancel();
|
||||||
|
_positionStreamSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +578,70 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
_startRealtimeLocationUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> loadRecords({bool scrollToTop = true}) async {
|
Future<void> loadRecords({bool scrollToTop = true}) async {
|
||||||
try {
|
try {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -1169,13 +1283,23 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final latestRoute = getValidRoute(latestRecord);
|
final latestRoute = getValidRoute(latestRecord);
|
||||||
final previousRoute =
|
|
||||||
previousRecord != null ? getValidRoute(previousRecord) : "";
|
|
||||||
|
|
||||||
final bool needsSpecialDisplay = previousRecord != null &&
|
String displayRoute = latestRoute;
|
||||||
latestRoute.isNotEmpty &&
|
bool isDisplayingLatestNormal = true;
|
||||||
previousRoute.isNotEmpty &&
|
|
||||||
latestRoute != previousRoute;
|
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 position = latestRecord.position.trim();
|
||||||
final speed = latestRecord.speed.trim();
|
final speed = latestRecord.speed.trim();
|
||||||
@@ -1203,10 +1327,10 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (latestRoute.isNotEmpty) ...[
|
if (displayRoute.isNotEmpty) ...[
|
||||||
if (needsSpecialDisplay) ...[
|
if (needsSpecialDisplay) ...[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(previousRoute,
|
child: Text(displayRoute,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -1221,18 +1345,25 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: const Color(0xFF1E1E1E),
|
||||||
title: const Text("路线变化",
|
title: const Text("路线信息",
|
||||||
style: TextStyle(color: Colors.white)),
|
style: TextStyle(color: Colors.white)),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("上一条: $previousRoute",
|
if (!isDisplayingLatestNormal) ...[
|
||||||
style: const TextStyle(color: Colors.grey)),
|
Text("显示路线: $displayRoute",
|
||||||
const SizedBox(height: 8),
|
style:
|
||||||
Text("当前: $latestRoute",
|
const TextStyle(color: Colors.white)),
|
||||||
style:
|
const SizedBox(height: 8),
|
||||||
const TextStyle(color: Colors.white)),
|
],
|
||||||
|
Text(
|
||||||
|
"最新路线: ${latestRoute.isNotEmpty ? latestRoute : '无效路线'}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: latestRoute.isNotEmpty
|
||||||
|
? Colors.grey
|
||||||
|
: Colors.red,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -1261,7 +1392,7 @@ class RealtimeScreenState extends State<RealtimeScreen> {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
] else
|
] else
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(latestRoute,
|
child: Text(displayRoute,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16, color: Colors.white),
|
fontSize: 16, color: Colors.white),
|
||||||
overflow: TextOverflow.ellipsis)),
|
overflow: TextOverflow.ellipsis)),
|
||||||
|
|||||||
@@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
|
|||||||
Reference in New Issue
Block a user