diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index cf5d496..30f3b64 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -1,11 +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 +31,9 @@ class HistoryScreenState extends State { final ScrollController _scrollController = ScrollController(); bool _isAtTop = true; MergeSettings _mergeSettings = MergeSettings(); + + final Map _mapOptimalZoom = {}; + final Map _mapCalculating = {}; int getSelectedCount() => _selectedRecords.length; Set getSelectedRecordIds() => _selectedRecords; @@ -336,8 +340,25 @@ class HistoryScreenState extends State { .whereType() .toList(); if (positions.isEmpty) return const SizedBox.shrink(); + + final mapId = records.map((r) => r.uniqueId).join('_'); final bounds = LatLngBounds.fromPoints(positions); - final optimalZoom = _calculateOptimalZoom(positions, containerWidth: 400, containerHeight: 220); + + 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; + }); + } + }); + } + + final zoomLevel = _mapOptimalZoom[mapId] ?? _getDefaultZoom(positions); + return Column(children: [ const SizedBox(height: 8), Container( @@ -348,7 +369,7 @@ class HistoryScreenState extends State { child: FlutterMap( options: MapOptions( initialCenter: bounds.center, - initialZoom: optimalZoom, + initialZoom: zoomLevel, minZoom: 5, maxZoom: 18), children: [ @@ -374,6 +395,12 @@ class HistoryScreenState extends State { ])) ]); } + + double _getDefaultZoom(List 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); @@ -638,4 +665,104 @@ class HistoryScreenState extends State { return null; } } + + Future<_BoundaryBox> _calculateBoundaryBoxParallel(List positions) async { + if (positions.isEmpty) { + return _BoundaryBox(0, 0, 0, 0); + } + + if (positions.length < 100) { + return _calculateBoundaryBoxIsolate(positions); + } + + final chunkSize = (positions.length / 4).ceil(); + final chunks = >[]; + + 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 _calculateOptimalZoomAsync(List positions, {required double containerWidth, required double containerHeight}) async { + if (positions.isEmpty) return 15.0; + 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(1.0, math.min(20.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 positions) { + if (positions.isEmpty) { + return _BoundaryBox(0, 0, 0, 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); + } + + return _BoundaryBox(minLat, maxLat, minLng, maxLng); } diff --git a/pubspec.yaml b/pubspec.yaml index 89e4be3..dfd1da3 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.1.5-flutter +version: 0.1.6-flutter environment: sdk: ^3.5.4