feat: optimize map zoom calculation and added location update
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
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';
|
||||||
@@ -256,6 +257,66 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _calculateOptimalZoom(List<LatLng> 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) {
|
String _getLocationInfo(TrainRecord record) {
|
||||||
List<String> parts = [];
|
List<String> parts = [];
|
||||||
if (record.route.isNotEmpty && record.route != "<NUL>")
|
if (record.route.isNotEmpty && record.route != "<NUL>")
|
||||||
@@ -276,6 +337,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
if (positions.isEmpty) return const SizedBox.shrink();
|
if (positions.isEmpty) return const SizedBox.shrink();
|
||||||
final bounds = LatLngBounds.fromPoints(positions);
|
final bounds = LatLngBounds.fromPoints(positions);
|
||||||
|
final optimalZoom = _calculateOptimalZoom(positions, containerWidth: 400, containerHeight: 220);
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
@@ -286,10 +348,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: bounds.center,
|
initialCenter: bounds.center,
|
||||||
initialZoom: 10,
|
initialZoom: optimalZoom,
|
||||||
minZoom: 5,
|
minZoom: 5,
|
||||||
maxZoom: 18,
|
maxZoom: 18),
|
||||||
cameraConstraint: CameraConstraint.contain(bounds: bounds)),
|
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate:
|
urlTemplate:
|
||||||
@@ -495,19 +556,29 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
|
|
||||||
Widget _buildExpandedContent(TrainRecord record) {
|
Widget _buildExpandedContent(TrainRecord record) {
|
||||||
final position = _parsePosition(record.positionInfo);
|
final position = _parsePosition(record.positionInfo);
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
if (position == null) return const SizedBox.shrink();
|
||||||
if (position != null)
|
|
||||||
Column(children: [
|
return FutureBuilder<double>(
|
||||||
|
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),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
height: 220,
|
height: 220,
|
||||||
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Colors.grey[900]),
|
color: Colors.grey[900]),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options:
|
options: MapOptions(
|
||||||
MapOptions(initialCenter: position, initialZoom: 15.0),
|
initialCenter: position,
|
||||||
|
initialZoom: zoomSnapshot.data!
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate:
|
urlTemplate:
|
||||||
@@ -528,8 +599,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
color: Colors.white, size: 20)))
|
color: Colors.white, size: 20)))
|
||||||
])
|
])
|
||||||
]))
|
]))
|
||||||
])
|
]);
|
||||||
]);
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LatLng? _parsePosition(String? positionInfo) {
|
LatLng? _parsePosition(String? positionInfo) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@@ -26,8 +27,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
bool _isMapInitialized = false;
|
bool _isMapInitialized = false;
|
||||||
bool _isFollowingLocation = false;
|
bool _isFollowingLocation = false;
|
||||||
bool _isLocationPermissionGranted = false;
|
bool _isLocationPermissionGranted = false;
|
||||||
|
Timer? _locationTimer;
|
||||||
|
|
||||||
static const LatLng _defaultPosition = LatLng(39.9042, 116.4074);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -35,12 +36,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_initializeMap();
|
_initializeMap();
|
||||||
_loadTrainRecords();
|
_loadTrainRecords();
|
||||||
_loadSettings();
|
_loadSettings();
|
||||||
_requestLocationPermission();
|
_startLocationUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
|
_locationTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
Future<void> _requestLocationPermission() async {
|
Future<void> _requestLocationPermission() async {
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请开启定位服务')),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +63,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +86,39 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_userLocation = LatLng(position.latitude, position.longitude);
|
_userLocation = LatLng(position.latitude, position.longitude);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!_isMapInitialized && _userLocation != null) {
|
} catch (e) {
|
||||||
_mapController.move(_userLocation!, _currentZoom);
|
}
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startLocationUpdates() {
|
||||||
|
_requestLocationPermission();
|
||||||
|
|
||||||
|
_locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||||
|
if (_isLocationPermissionGranted) {
|
||||||
|
_getCurrentLocation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _loadSettings() async {
|
Future<void> _loadSettings() async {
|
||||||
try {
|
try {
|
||||||
final settings = await DatabaseService.instance.getAllSettings();
|
final settings = await DatabaseService.instance.getAllSettings();
|
||||||
@@ -159,13 +194,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
} else if (_lastTrainLocation != null) {
|
} else if (_lastTrainLocation != null) {
|
||||||
targetLocation = _lastTrainLocation;
|
targetLocation = _lastTrainLocation;
|
||||||
} else {
|
} else {
|
||||||
targetLocation = _defaultPosition;
|
_isMapInitialized = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_centerMap(targetLocation!, zoom: _currentZoom);
|
||||||
_centerMap(targetLocation!, zoom: _currentZoom);
|
_isMapInitialized = true;
|
||||||
_isMapInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _centerMap(LatLng location, {double? zoom}) {
|
void _centerMap(LatLng location, {double? zoom}) {
|
||||||
@@ -313,7 +347,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _centerToMyLocation() {
|
void _centerToMyLocation() {
|
||||||
_centerMap(_lastTrainLocation ?? _defaultPosition, zoom: 15.0);
|
_centerMap(_lastTrainLocation ?? const LatLng(39.9042, 116.4074), zoom: 15.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _centerToLastTrain() {
|
void _centerToLastTrain() {
|
||||||
@@ -537,11 +571,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
FlutterMap(
|
FlutterMap(
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: _lastTrainLocation ?? _defaultPosition,
|
initialCenter: _lastTrainLocation ?? 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) {
|
if (hasGesture) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -552,28 +587,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_saveSettings();
|
_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: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -622,10 +635,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
heroTag: 'myLocation',
|
heroTag: 'myLocation',
|
||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: const Color(0xFF1E1E1E),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_getCurrentLocation();
|
_forceUpdateLocation();
|
||||||
if (_userLocation != null) {
|
|
||||||
_centerMap(_userLocation!, zoom: 15.0);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.my_location, color: Colors.white),
|
child: const Icon(Icons.my_location, color: Colors.white),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user