From 5141af58acf00f97381e292e2b23ac516209099d Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Wed, 24 Sep 2025 16:13:03 +0800 Subject: [PATCH] feat: optimize map zoom calculation and added location update --- lib/screens/history_screen.dart | 92 +++++++++++++++++++++++++++++---- lib/screens/map_screen.dart | 88 +++++++++++++++++-------------- 2 files changed, 131 insertions(+), 49 deletions(-) diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index 3454cca..cf5d496 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -256,6 +257,66 @@ class HistoryScreenState extends State { } } + double _calculateOptimalZoom(List positions, {double containerWidth = 400, double containerHeight = 220}) { + if (positions.isEmpty) return 15.0; + if (positions.length == 1) return 17.0; + + double minLat = positions[0].latitude; + double maxLat = positions[0].latitude; + double minLng = positions[0].longitude; + double maxLng = positions[0].longitude; + + for (final pos in positions) { + minLat = math.min(minLat, pos.latitude); + maxLat = math.max(maxLat, pos.latitude); + minLng = math.min(minLng, pos.longitude); + maxLng = math.max(maxLng, pos.longitude); + } + + double latToY(double lat) { + final latRad = lat * math.pi / 180.0; + return math.log(math.tan(latRad) + 1.0/math.cos(latRad)); + } + + double lngToX(double lng) { + return lng * math.pi / 180.0; + } + + final minX = lngToX(minLng); + final maxX = lngToX(maxLng); + final minY = latToY(minLat); + final maxY = latToY(maxLat); + + const worldSize = 2.0 * math.pi; + + final widthWorld = (maxX - minX) / worldSize; + final heightWorld = (maxY - minY) / worldSize; + + const paddingRatio = 0.8; + + final widthZoom = math.log((containerWidth * paddingRatio) / (widthWorld * 256.0)) / math.log(2.0); + final heightZoom = math.log((containerHeight * paddingRatio) / (heightWorld * 256.0)) / math.log(2.0); + + final optimalZoom = math.min(widthZoom, heightZoom); + + return math.max(1.0, math.min(20.0, optimalZoom)); + } + + double _calculateDistance(LatLng pos1, LatLng pos2) { + const earthRadius = 6371000; + final lat1 = pos1.latitude * math.pi / 180; + final lat2 = pos2.latitude * math.pi / 180; + final deltaLat = (pos2.latitude - pos1.latitude) * math.pi / 180; + final deltaLng = (pos2.longitude - pos1.longitude) * math.pi / 180; + + final a = math.sin(deltaLat / 2) * math.sin(deltaLat / 2) + + math.cos(lat1) * math.cos(lat2) * + math.sin(deltaLng / 2) * math.sin(deltaLng / 2); + final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + + return earthRadius * c; + } + String _getLocationInfo(TrainRecord record) { List parts = []; if (record.route.isNotEmpty && record.route != "") @@ -276,6 +337,7 @@ class HistoryScreenState extends State { .toList(); if (positions.isEmpty) return const SizedBox.shrink(); final bounds = LatLngBounds.fromPoints(positions); + final optimalZoom = _calculateOptimalZoom(positions, containerWidth: 400, containerHeight: 220); return Column(children: [ const SizedBox(height: 8), Container( @@ -286,10 +348,9 @@ class HistoryScreenState extends State { child: FlutterMap( options: MapOptions( initialCenter: bounds.center, - initialZoom: 10, + initialZoom: optimalZoom, minZoom: 5, - maxZoom: 18, - cameraConstraint: CameraConstraint.contain(bounds: bounds)), + maxZoom: 18), children: [ TileLayer( urlTemplate: @@ -495,19 +556,29 @@ class HistoryScreenState extends State { Widget _buildExpandedContent(TrainRecord record) { final position = _parsePosition(record.positionInfo); - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (position != null) - Column(children: [ + if (position == null) return const SizedBox.shrink(); + + return FutureBuilder( + future: Future(() => _calculateOptimalZoom([position], containerWidth: 400, containerHeight: 220)), + builder: (context, zoomSnapshot) { + if (!zoomSnapshot.hasData) { + return const SizedBox.shrink(); + } + + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), Container( height: 220, + width: double.infinity, margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Colors.grey[900]), child: FlutterMap( - options: - MapOptions(initialCenter: position, initialZoom: 15.0), + options: MapOptions( + initialCenter: position, + initialZoom: zoomSnapshot.data! + ), children: [ TileLayer( urlTemplate: @@ -528,8 +599,9 @@ class HistoryScreenState extends State { color: Colors.white, size: 20))) ]) ])) - ]) - ]); + ]); + }, + ); } LatLng? _parsePosition(String? positionInfo) { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 89dec73..6984827 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -26,8 +27,8 @@ class _MapScreenState extends State { bool _isMapInitialized = false; bool _isFollowingLocation = false; bool _isLocationPermissionGranted = false; + Timer? _locationTimer; - static const LatLng _defaultPosition = LatLng(39.9042, 116.4074); @override void initState() { @@ -35,12 +36,13 @@ class _MapScreenState extends State { _initializeMap(); _loadTrainRecords(); _loadSettings(); - _requestLocationPermission(); + _startLocationUpdates(); } @override void dispose() { _saveSettings(); + _locationTimer?.cancel(); super.dispose(); } @@ -49,6 +51,9 @@ class _MapScreenState extends State { Future _requestLocationPermission() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请开启定位服务')), + ); return; } @@ -58,6 +63,9 @@ class _MapScreenState extends State { } if (permission == LocationPermission.deniedForever) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')), + ); return; } @@ -78,12 +86,39 @@ class _MapScreenState extends State { _userLocation = LatLng(position.latitude, position.longitude); }); - if (!_isMapInitialized && _userLocation != null) { - _mapController.move(_userLocation!, _currentZoom); - } - } catch (e) {} + } catch (e) { + } } + void _startLocationUpdates() { + _requestLocationPermission(); + + _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) { + if (_isLocationPermissionGranted) { + _getCurrentLocation(); + } + }); + } + + Future _forceUpdateLocation() async { + + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.best, + ); + + final newLocation = LatLng(position.latitude, position.longitude); + + setState(() { + _userLocation = newLocation; + }); + + _mapController.move(newLocation, 15.0); + } catch (e) { + } + } + + Future _loadSettings() async { try { final settings = await DatabaseService.instance.getAllSettings(); @@ -159,13 +194,12 @@ class _MapScreenState extends State { } else if (_lastTrainLocation != null) { targetLocation = _lastTrainLocation; } else { - targetLocation = _defaultPosition; + _isMapInitialized = true; + return; } - WidgetsBinding.instance.addPostFrameCallback((_) { - _centerMap(targetLocation!, zoom: _currentZoom); - _isMapInitialized = true; - }); + _centerMap(targetLocation!, zoom: _currentZoom); + _isMapInitialized = true; } void _centerMap(LatLng location, {double? zoom}) { @@ -313,7 +347,7 @@ class _MapScreenState extends State { } void _centerToMyLocation() { - _centerMap(_lastTrainLocation ?? _defaultPosition, zoom: 15.0); + _centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), zoom: 15.0); } void _centerToLastTrain() { @@ -537,11 +571,12 @@ class _MapScreenState extends State { FlutterMap( mapController: _mapController, options: MapOptions( - initialCenter: _lastTrainLocation ?? _defaultPosition, + initialCenter: _lastTrainLocation ?? const LatLng(39.9042, 116.4074), initialZoom: _currentZoom, initialRotation: _currentRotation, minZoom: 4.0, maxZoom: 18.0, + onPositionChanged: (MapCamera camera, bool hasGesture) { if (hasGesture) { setState(() { @@ -552,28 +587,6 @@ class _MapScreenState extends State { _saveSettings(); } }, - onTap: (_, point) { - for (final record in _trainRecords) { - final coords = record.getCoordinates(); - final dmsCoords = _parseDmsCoordinate(record.positionInfo); - LatLng? recordPosition; - - if (dmsCoords != null) { - recordPosition = dmsCoords; - } else if (coords['lat'] != 0.0 && coords['lng'] != 0.0) { - recordPosition = LatLng(coords['lat']!, coords['lng']!); - } - - if (recordPosition != null) { - final distance = const Distance() - .as(LengthUnit.Meter, recordPosition, point); - if (distance < 50) { - _showTrainDetailsDialog(record, recordPosition); - break; - } - } - } - }, ), children: [ TileLayer( @@ -622,10 +635,7 @@ class _MapScreenState extends State { heroTag: 'myLocation', backgroundColor: const Color(0xFF1E1E1E), onPressed: () { - _getCurrentLocation(); - if (_userLocation != null) { - _centerMap(_userLocation!, zoom: 15.0); - } + _forceUpdateLocation(); }, child: const Icon(Icons.my_location, color: Colors.white), ),