Compare commits
3 Commits
v0.1.5-flu
...
v0.1.7-flu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10825171fd | ||
|
|
1b05a6092c | ||
|
|
5141af58ac |
@@ -1,10 +1,12 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:lbjconsole/models/merged_record.dart';
|
||||
import 'package:lbjconsole/services/database_service.dart';
|
||||
import 'package:lbjconsole/models/train_record.dart';
|
||||
import 'package:lbjconsole/services/merge_service.dart';
|
||||
import '../models/merged_record.dart';
|
||||
import '../services/database_service.dart';
|
||||
import '../models/train_record.dart';
|
||||
import '../services/merge_service.dart';
|
||||
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
final Function(bool isEditing) onEditModeChanged;
|
||||
@@ -30,6 +32,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
bool _isAtTop = true;
|
||||
MergeSettings _mergeSettings = MergeSettings();
|
||||
|
||||
final Map<String, double> _mapOptimalZoom = {};
|
||||
final Map<String, bool> _mapCalculating = {};
|
||||
|
||||
int getSelectedCount() => _selectedRecords.length;
|
||||
Set<String> getSelectedRecordIds() => _selectedRecords;
|
||||
List<Object> getDisplayItems() => _displayItems;
|
||||
@@ -151,8 +156,19 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
widget.onSelectionChanged();
|
||||
});
|
||||
} else {
|
||||
setState(
|
||||
() => _expandedStates[mergedRecord.groupKey] = !isExpanded);
|
||||
if (isExpanded) {
|
||||
final mapId =
|
||||
mergedRecord.records.map((r) => r.uniqueId).join('_');
|
||||
setState(() {
|
||||
_expandedStates[mergedRecord.groupKey] = false;
|
||||
_mapOptimalZoom.remove(mapId);
|
||||
_mapCalculating.remove(mapId);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_expandedStates[mergedRecord.groupKey] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
@@ -256,6 +272,72 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
double _calculateOptimalZoom(List<LatLng> positions,
|
||||
{double containerWidth = 400, double containerHeight = 220}) {
|
||||
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<String> parts = [];
|
||||
if (record.route.isNotEmpty && record.route != "<NUL>")
|
||||
@@ -263,7 +345,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
if (record.direction != 0) parts.add(record.direction == 1 ? "下" : "上");
|
||||
if (record.position.isNotEmpty && record.position != "<NUL>") {
|
||||
final position = record.position;
|
||||
final cleanPosition = position.endsWith('.') ? position.substring(0, position.length - 1) : position;
|
||||
final cleanPosition = position.endsWith('.')
|
||||
? position.substring(0, position.length - 1)
|
||||
: position;
|
||||
parts.add("${cleanPosition}K");
|
||||
}
|
||||
return parts.join(' ');
|
||||
@@ -275,7 +359,45 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
.whereType<LatLng>()
|
||||
.toList();
|
||||
if (positions.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final mapId = records.map((r) => r.uniqueId).join('_');
|
||||
final bounds = LatLngBounds.fromPoints(positions);
|
||||
|
||||
if (!_mapOptimalZoom.containsKey(mapId) &&
|
||||
!(_mapCalculating[mapId] ?? false)) {
|
||||
_mapCalculating[mapId] = true;
|
||||
|
||||
_calculateOptimalZoomAsync(positions,
|
||||
containerWidth: 400, containerHeight: 220)
|
||||
.then((optimalZoom) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mapOptimalZoom[mapId] = optimalZoom;
|
||||
_mapCalculating[mapId] = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!_mapOptimalZoom.containsKey(mapId)) {
|
||||
return const Column(
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 228,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.blue,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final zoomLevel = _mapOptimalZoom[mapId]!;
|
||||
|
||||
return Column(children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
@@ -283,37 +405,21 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8), color: Colors.grey[900]),
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: bounds.center,
|
||||
initialZoom: 10,
|
||||
minZoom: 5,
|
||||
maxZoom: 18,
|
||||
cameraConstraint: CameraConstraint.contain(bounds: bounds)),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'org.noxylva.lbjconsole'),
|
||||
MarkerLayer(
|
||||
markers: positions
|
||||
.map((pos) => Marker(
|
||||
point: pos,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white, width: 2)),
|
||||
child: const Icon(Icons.train,
|
||||
color: Colors.white, size: 20))))
|
||||
.toList())
|
||||
]))
|
||||
child: _DelayedMultiMarkerMap(
|
||||
key: ValueKey('multi_map_${mapId}_$zoomLevel'),
|
||||
positions: positions,
|
||||
center: bounds.center,
|
||||
zoom: zoomLevel,
|
||||
))
|
||||
]);
|
||||
}
|
||||
|
||||
double _getDefaultZoom(List<LatLng> positions) {
|
||||
if (positions.length == 1) return 15.0;
|
||||
if (positions.length < 10) return 12.0;
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
Widget _buildRecordCard(TrainRecord record, {bool isSubCard = false}) {
|
||||
final isSelected = _selectedRecords.contains(record.uniqueId);
|
||||
final isExpanded =
|
||||
@@ -344,7 +450,17 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
widget.onSelectionChanged();
|
||||
});
|
||||
} else if (!isSubCard) {
|
||||
setState(() => _expandedStates[record.uniqueId] = !isExpanded);
|
||||
if (isExpanded) {
|
||||
setState(() {
|
||||
_expandedStates[record.uniqueId] = false;
|
||||
_mapOptimalZoom.remove(record.uniqueId);
|
||||
_mapCalculating.remove(record.uniqueId);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_expandedStates[record.uniqueId] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
@@ -481,7 +597,8 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
if (isValidRoute && isValidPosition) const SizedBox(width: 4),
|
||||
if (isValidPosition)
|
||||
Flexible(
|
||||
child: Text("${position.trim().endsWith('.') ? position.trim().substring(0, position.trim().length - 1) : position.trim()}K",
|
||||
child: Text(
|
||||
"${position.trim().endsWith('.') ? position.trim().substring(0, position.trim().length - 1) : position.trim()}K",
|
||||
style:
|
||||
const TextStyle(fontSize: 16, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis))
|
||||
@@ -495,40 +612,59 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
|
||||
Widget _buildExpandedContent(TrainRecord record) {
|
||||
final position = _parsePosition(record.positionInfo);
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (position != null)
|
||||
Column(children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 220,
|
||||
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),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'org.noxylva.lbjconsole'),
|
||||
MarkerLayer(markers: [
|
||||
Marker(
|
||||
point: position,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.white, width: 2)),
|
||||
child: const Icon(Icons.train,
|
||||
color: Colors.white, size: 20)))
|
||||
])
|
||||
]))
|
||||
])
|
||||
final mapId = record.uniqueId;
|
||||
|
||||
if (position == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!_mapOptimalZoom.containsKey(mapId) &&
|
||||
!(_mapCalculating[mapId] ?? false)) {
|
||||
_mapCalculating[mapId] = true;
|
||||
|
||||
_calculateOptimalZoomAsync([position],
|
||||
containerWidth: 400, containerHeight: 220)
|
||||
.then((optimalZoom) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mapOptimalZoom[mapId] = optimalZoom;
|
||||
_mapCalculating[mapId] = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!_mapOptimalZoom.containsKey(mapId)) {
|
||||
return const Column(
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 228,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.blue,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final zoomLevel = _mapOptimalZoom[mapId]!;
|
||||
|
||||
return Column(children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 220,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8), color: Colors.grey[900]),
|
||||
child: _DelayedMapWithMarker(
|
||||
key: ValueKey('map_${mapId}_$zoomLevel'),
|
||||
position: position,
|
||||
zoom: zoomLevel,
|
||||
))
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -566,4 +702,192 @@ class HistoryScreenState extends State<HistoryScreen> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<_BoundaryBox> _calculateBoundaryBoxParallel(
|
||||
List<LatLng> positions) async {
|
||||
if (positions.length < 100) {
|
||||
return _calculateBoundaryBoxIsolate(positions);
|
||||
}
|
||||
|
||||
final chunkSize = (positions.length / 4).ceil();
|
||||
final chunks = <List<LatLng>>[];
|
||||
|
||||
for (int i = 0; i < positions.length; i += chunkSize) {
|
||||
final end = math.min(i + chunkSize, positions.length);
|
||||
chunks.add(positions.sublist(i, end));
|
||||
}
|
||||
|
||||
final results = await Future.wait(chunks.map(
|
||||
(chunk) => Isolate.run(() => _calculateBoundaryBoxIsolate(chunk))));
|
||||
|
||||
double minLat = results[0].minLat;
|
||||
double maxLat = results[0].maxLat;
|
||||
double minLng = results[0].minLng;
|
||||
double maxLng = results[0].maxLng;
|
||||
|
||||
for (final box in results.skip(1)) {
|
||||
minLat = math.min(minLat, box.minLat);
|
||||
maxLat = math.max(maxLat, box.maxLat);
|
||||
minLng = math.min(minLng, box.minLng);
|
||||
maxLng = math.max(maxLng, box.maxLng);
|
||||
}
|
||||
|
||||
return _BoundaryBox(minLat, maxLat, minLng, maxLng);
|
||||
}
|
||||
|
||||
Future<double> _calculateOptimalZoomAsync(List<LatLng> positions,
|
||||
{required double containerWidth, required double containerHeight}) async {
|
||||
if (positions.length == 1) return 17.0;
|
||||
|
||||
final boundaryBox = await _calculateBoundaryBoxParallel(positions);
|
||||
|
||||
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(boundaryBox.minLng);
|
||||
final maxX = lngToX(boundaryBox.maxLng);
|
||||
final minY = latToY(boundaryBox.minLat);
|
||||
final maxY = latToY(boundaryBox.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(5.0, math.min(18.0, optimalZoom));
|
||||
}
|
||||
}
|
||||
|
||||
class _BoundaryBox {
|
||||
final double minLat;
|
||||
final double maxLat;
|
||||
final double minLng;
|
||||
final double maxLng;
|
||||
|
||||
_BoundaryBox(this.minLat, this.maxLat, this.minLng, this.maxLng);
|
||||
}
|
||||
|
||||
_BoundaryBox _calculateBoundaryBoxIsolate(List<LatLng> positions) {
|
||||
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);
|
||||
}
|
||||
|
||||
return _BoundaryBox(minLat, maxLat, minLng, maxLng);
|
||||
}
|
||||
|
||||
class _DelayedMapWithMarker extends StatefulWidget {
|
||||
final LatLng position;
|
||||
final double zoom;
|
||||
|
||||
const _DelayedMapWithMarker({
|
||||
Key? key,
|
||||
required this.position,
|
||||
required this.zoom,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState();
|
||||
}
|
||||
|
||||
class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlutterMap(
|
||||
options:
|
||||
MapOptions(initialCenter: widget.position, initialZoom: widget.zoom),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'org.noxylva.lbjconsole'),
|
||||
MarkerLayer(markers: [
|
||||
Marker(
|
||||
point: widget.position,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white, width: 2)),
|
||||
child:
|
||||
const Icon(Icons.train, color: Colors.white, size: 20)))
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DelayedMultiMarkerMap extends StatefulWidget {
|
||||
final List<LatLng> positions;
|
||||
final LatLng center;
|
||||
final double zoom;
|
||||
|
||||
const _DelayedMultiMarkerMap({
|
||||
Key? key,
|
||||
required this.positions,
|
||||
required this.center,
|
||||
required this.zoom,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState();
|
||||
}
|
||||
|
||||
class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: widget.center,
|
||||
initialZoom: widget.zoom,
|
||||
minZoom: 5,
|
||||
maxZoom: 18,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'org.noxylva.lbjconsole',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: widget.positions
|
||||
.map((pos) => Marker(
|
||||
point: pos,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2)),
|
||||
child: const Icon(Icons.train,
|
||||
color: Colors.white, size: 20))))
|
||||
.toList()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MapScreen> {
|
||||
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<MapScreen> {
|
||||
_initializeMap();
|
||||
_loadTrainRecords();
|
||||
_loadSettings();
|
||||
_requestLocationPermission();
|
||||
_startLocationUpdates();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_saveSettings();
|
||||
_locationTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -49,6 +51,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Future<void> _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<MapScreen> {
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('定位权限被拒绝,请在设置中开启')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,12 +86,39 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_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<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 {
|
||||
try {
|
||||
final settings = await DatabaseService.instance.getAllSettings();
|
||||
@@ -159,13 +194,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
} 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<MapScreen> {
|
||||
}
|
||||
|
||||
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<MapScreen> {
|
||||
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<MapScreen> {
|
||||
_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<MapScreen> {
|
||||
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),
|
||||
),
|
||||
|
||||
@@ -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.1.5-flutter
|
||||
version: 0.1.7-flutter
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
|
||||
Reference in New Issue
Block a user