5 Commits

Author SHA1 Message Date
Nedifinita
24c6abd4f3 Merge branch 'flutter' of https://github.com/undef-i/LBJ_Console into flutter 2025-10-08 22:35:47 +08:00
Nedifinita
5b3960f7d6 fix: correct the scrolling status error when adding new records to the merged record group 2025-10-08 22:35:43 +08:00
undef-i
33e790957e Add .gitattributes file 2025-09-30 01:11:08 +08:00
undef-i
88e3636c3f Create .gitattributes 2025-09-30 01:10:34 +08:00
Nedifinita
cc2a495984 feat: improve map initialization process and communication 2025-09-29 20:17:05 +08:00
6 changed files with 313 additions and 349 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
assets/* linguist-vendored

View File

@@ -14848,8 +14848,8 @@
el.addEventListener("click", function (e) { el.addEventListener("click", function (e) {
e.stopPropagation(); e.stopPropagation();
showTrainDetails(lat, lng, recordData); showTrainDetails(lat, lng, recordData);
if (window.flutter_inappwebview) { if (window.showTrainDetails) {
window.flutter_inappwebview.callHandler("showTrainDetails", { window.showTrainDetails.postMessage(JSON.stringify({
id: recordData.id || Date.now(), id: recordData.id || Date.now(),
trainNumber: trainNumber || "未知车次", trainNumber: trainNumber || "未知车次",
trainType: recordData.trainType || "未知类型", trainType: recordData.trainType || "未知类型",
@@ -14858,7 +14858,7 @@
latitude: lat, latitude: lat,
longitude: lng, longitude: lng,
timestamp: Date.now(), timestamp: Date.now(),
}); }));
} }
}); });
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
@@ -14931,11 +14931,21 @@
.addTo(map); .addTo(map);
} }
function setCenter(lat, lng, zoom, bearing) { function setCenter(lat, lng, zoom, bearing) {
map.jumpTo({ if (!window.map) {
console.error("[JS] Map object not ready in setCenter");
return;
}
window.map.jumpTo({
center: [lng, lat], center: [lng, lat],
zoom: zoom !== undefined ? zoom : map.getZoom(), zoom: zoom !== undefined ? zoom : window.map.getZoom(),
bearing: bearing !== undefined ? bearing : map.getBearing(), bearing: bearing !== undefined ? bearing : window.map.getBearing(),
}); });
const mapContainer = window.map.getContainer();
if (mapContainer.style.opacity === "0") {
mapContainer.style.opacity = "1";
}
} }
function getMapState() { function getMapState() {
const center = map.getCenter(); const center = map.getCenter();
@@ -14948,17 +14958,19 @@
} }
function setupMapEventListeners() { function setupMapEventListeners() {
if (!map) return; if (!map) return;
map.on("moveend", function () { map.on("move", function () {
const center = map.getCenter(); const center = map.getCenter();
const zoom = map.getZoom(); const zoom = map.getZoom();
const bearing = map.getBearing(); const bearing = map.getBearing();
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler("onMapStateChanged", { if (window.onMapStateChanged) {
const mapState = JSON.stringify({
lat: center.lat, lat: center.lat,
lng: center.lng, lng: center.lng,
zoom: zoom, zoom: zoom,
bearing: bearing, bearing: bearing,
}); });
window.onMapStateChanged.postMessage(mapState);
} }
}); });
} }

View File

@@ -1,9 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:isolate'; import 'dart:isolate';
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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 'package:geolocator/geolocator.dart';
@@ -33,6 +31,10 @@ class HistoryScreenState extends State<HistoryScreen> {
final List<Object> _displayItems = []; final List<Object> _displayItems = [];
bool _isLoading = true; bool _isLoading = true;
bool _isEditMode = false; bool _isEditMode = false;
int? _anchorIndex;
double? _anchorOffset;
double? _oldCardHeight;
double? _oldScrollOffset;
final Set<String> _selectedRecords = {}; final Set<String> _selectedRecords = {};
final Map<String, bool> _expandedStates = {}; final Map<String, bool> _expandedStates = {};
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@@ -41,7 +43,6 @@ class HistoryScreenState extends State<HistoryScreen> {
late final ChatScrollObserver _chatObserver; late final ChatScrollObserver _chatObserver;
bool _isAtTop = true; bool _isAtTop = true;
MergeSettings _mergeSettings = MergeSettings(); MergeSettings _mergeSettings = MergeSettings();
double _itemHeightCache = 0.0;
final Map<String, double> _mapOptimalZoom = {}; final Map<String, double> _mapOptimalZoom = {};
final Map<String, bool> _mapCalculating = {}; final Map<String, bool> _mapCalculating = {};
@@ -74,15 +75,21 @@ class HistoryScreenState extends State<HistoryScreen> {
super.initState(); super.initState();
_chatObserver = ChatScrollObserver(_observerController) _chatObserver = ChatScrollObserver(_observerController)
..toRebuildScrollViewCallback = () { ..toRebuildScrollViewCallback = () {
setState(() {}); if (mounted) {
setState(() {});
}
}; };
_scrollController.addListener(() { _scrollController.addListener(() {
if (_scrollController.position.atEdge) { if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0) { if (_scrollController.position.pixels == 0) {
if (!_isAtTop) setState(() => _isAtTop = true); if (!_isAtTop) {
setState(() => _isAtTop = true);
}
} }
} else { } else {
if (_isAtTop) setState(() => _isAtTop = false); if (_isAtTop) {
setState(() => _isAtTop = false);
}
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -109,17 +116,19 @@ class HistoryScreenState extends State<HistoryScreen> {
List<TrainRecord> filteredRecords = allRecords; List<TrainRecord> filteredRecords = allRecords;
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) { if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {
int hiddenCount = 0;
int shownCount = 0;
filteredRecords = allRecords.where((record) { filteredRecords = allRecords.where((record) {
bool isFieldMeaningful(String field) { bool isFieldMeaningful(String field) {
if (field.isEmpty) return false; if (field.isEmpty) {
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) return false;
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
return false; return false;
}
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) {
return false;
}
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first)) {
return false;
}
return true; return true;
} }
@@ -160,12 +169,6 @@ class HistoryScreenState extends State<HistoryScreen> {
hasLbjClass || hasLbjClass ||
hasTrain; hasTrain;
if (!shouldShow) {
hiddenCount++;
} else {
shownCount++;
}
return shouldShow; return shouldShow;
}).toList(); }).toList();
} }
@@ -192,7 +195,9 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
} }
} catch (e) { } catch (e) {
if (mounted) setState(() => _isLoading = false); if (mounted) {
setState(() => _isLoading = false);
}
} }
} }
@@ -201,57 +206,7 @@ class HistoryScreenState extends State<HistoryScreen> {
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {}; final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
_mergeSettings = MergeSettings.fromMap(settingsMap); _mergeSettings = MergeSettings.fromMap(settingsMap);
if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) { if ((settingsMap['hideTimeOnlyRecords'] ?? 0) == 1) {}
bool isFieldMeaningful(String field) {
if (field.isEmpty) return false;
String cleaned = field.replaceAll('<NUL>', '').trim();
if (cleaned.isEmpty) return false;
if (cleaned.runes
.every((r) => r == '*'.runes.first || r == ' '.runes.first))
return false;
return true;
}
final hasTrainNumber = isFieldMeaningful(newRecord.fullTrainNumber) &&
!newRecord.fullTrainNumber.contains("-----");
final hasDirection =
newRecord.direction == 1 || newRecord.direction == 3;
final hasLocoInfo = isFieldMeaningful(newRecord.locoType) ||
isFieldMeaningful(newRecord.loco);
final hasRoute = isFieldMeaningful(newRecord.route);
final hasPosition = isFieldMeaningful(newRecord.position);
final hasSpeed =
isFieldMeaningful(newRecord.speed) && newRecord.speed != "NUL";
final hasPositionInfo = isFieldMeaningful(newRecord.positionInfo);
final hasTrainType = isFieldMeaningful(newRecord.trainType) &&
newRecord.trainType != "未知";
final hasLbjClass =
isFieldMeaningful(newRecord.lbjClass) && newRecord.lbjClass != "NA";
final hasTrain = isFieldMeaningful(newRecord.train) &&
!newRecord.train.contains("-----");
if (!hasTrainNumber &&
!hasDirection &&
!hasLocoInfo &&
!hasRoute &&
!hasPosition &&
!hasSpeed &&
!hasPositionInfo &&
!hasTrainType &&
!hasLbjClass &&
!hasTrain) {
return;
}
}
final isNewRecord = !_displayItems.any((item) { final isNewRecord = !_displayItems.any((item) {
if (item is TrainRecord) { if (item is TrainRecord) {
@@ -261,32 +216,132 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
return false; return false;
}); });
if (!isNewRecord) return; if (!isNewRecord) return;
final allRecords = await DatabaseService.instance.getAllRecords();
final items = MergeService.getMixedList(allRecords, _mergeSettings);
if (mounted) { if (mounted) {
if (!_isAtTop) { if (_isAtTop) {
_chatObserver.standby();
}
final hasDataChanged = _hasDataChanged(items);
if (hasDataChanged) {
setState(() { setState(() {
_displayItems.clear(); bool isMerge = false;
_displayItems.addAll(items); Object? mergeResult;
if (_displayItems.isNotEmpty) {
final firstItem = _displayItems.first;
List<TrainRecord> tempRecords = [newRecord];
if (firstItem is MergedTrainRecord) {
tempRecords.addAll(firstItem.records);
} else if (firstItem is TrainRecord) {
tempRecords.add(firstItem);
}
final mergeCheckResult =
MergeService.getMixedList(tempRecords, _mergeSettings);
if (mergeCheckResult.length == 1 &&
mergeCheckResult.first is MergedTrainRecord) {
isMerge = true;
mergeResult = mergeCheckResult.first;
}
}
if (isMerge) {
_displayItems[0] = mergeResult!;
} else {
_displayItems.insert(0, newRecord);
}
}); });
if (_scrollController.hasClients) {
_scrollController.jumpTo(0.0);
}
return;
} }
if (_isAtTop && _scrollController.hasClients) { final anchorModel = _observerController.observeFirstItem();
_scrollController.jumpTo(0.0); if (anchorModel == null) {
return;
} }
_anchorIndex = anchorModel.index;
if (_anchorIndex! > 0) {
_anchorOffset = anchorModel.layoutOffset;
} else {
_oldCardHeight = anchorModel.size.height;
_oldScrollOffset = _scrollController.offset;
}
bool isMerge = false;
Object? mergeResult;
final firstItem = _displayItems.first;
List<TrainRecord> tempRecords = [newRecord];
if (firstItem is MergedTrainRecord) {
tempRecords.addAll(firstItem.records);
} else if (firstItem is TrainRecord) {
tempRecords.add(firstItem);
}
final mergeCheckResult =
MergeService.getMixedList(tempRecords, _mergeSettings);
if (mergeCheckResult.length == 1 &&
mergeCheckResult.first is MergedTrainRecord) {
isMerge = true;
mergeResult = mergeCheckResult.first;
}
setState(() {
if (isMerge) {
_displayItems[0] = mergeResult!;
} else {
_displayItems.insert(0, newRecord);
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _anchorIndex == null) return;
if (_anchorIndex! > 0) {
final newAnchorIndex = isMerge ? _anchorIndex! : _anchorIndex! + 1;
final newAnchorModel =
_observerController.observeItem(index: newAnchorIndex);
if (newAnchorModel != null && _anchorOffset != null) {
final newOffset = newAnchorModel.layoutOffset;
final delta = newOffset - _anchorOffset!;
if (delta.abs() > 0.1) {
_scrollController.jumpTo(_scrollController.offset + delta);
}
}
} else {
final newAnchorModel = _observerController.observeItem(index: 0);
if (newAnchorModel != null &&
_oldCardHeight != null &&
_oldScrollOffset != null) {
final newHeight = newAnchorModel.size.height;
final heightDelta = newHeight - _oldCardHeight!;
if (heightDelta.abs() > 0.1) {
_scrollController.jumpTo(_oldScrollOffset! + heightDelta);
}
}
}
_anchorIndex = null;
_anchorOffset = null;
_oldCardHeight = null;
_oldScrollOffset = null;
});
} }
} catch (e) {} } catch (e) {}
} }
String _getGroupKeyForRecord(TrainRecord record, MergeSettings settings) {
switch (settings.groupBy) {
case GroupBy.trainOnly:
return record.train.trim();
case GroupBy.locoOnly:
return record.loco.trim();
case GroupBy.trainAndLoco:
return '${record.train.trim()}-${record.loco.trim()}';
case GroupBy.trainOrLoco:
final train = record.train.trim();
if (train.isNotEmpty) return train;
final loco = record.loco.trim();
if (loco.isNotEmpty) return loco;
return '';
}
}
bool _hasDataChanged(List<Object> newItems) { bool _hasDataChanged(List<Object> newItems) {
if (_displayItems.length != newItems.length) return true; if (_displayItems.length != newItems.length) return true;
@@ -303,7 +358,6 @@ class HistoryScreenState extends State<HistoryScreen> {
if (oldItem.records.length != newItem.records.length) return true; if (oldItem.records.length != newItem.records.length) return true;
} }
} }
return false; return false;
} }
@@ -346,6 +400,7 @@ class HistoryScreenState extends State<HistoryScreen> {
mergedRecord.records.any((r) => _selectedRecords.contains(r.uniqueId)); mergedRecord.records.any((r) => _selectedRecords.contains(r.uniqueId));
final isExpanded = _expandedStates[mergedRecord.groupKey] ?? false; final isExpanded = _expandedStates[mergedRecord.groupKey] ?? false;
return Card( return Card(
key: ValueKey(mergedRecord.groupKey),
color: isSelected && _isEditMode color: isSelected && _isEditMode
? const Color(0xFF2E2E2E) ? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E), : const Color(0xFF1E1E1E),
@@ -389,7 +444,9 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
}, },
onLongPress: () { onLongPress: () {
if (!_isEditMode) setEditMode(true); if (!_isEditMode) {
setEditMode(true);
}
setState(() { setState(() {
final allIdsInGroup = final allIdsInGroup =
mergedRecord.records.map((r) => r.uniqueId).toSet(); mergedRecord.records.map((r) => r.uniqueId).toSet();
@@ -488,10 +545,8 @@ class HistoryScreenState extends State<HistoryScreen> {
TrainRecord record, TrainRecord latest, GroupBy groupBy) { TrainRecord record, TrainRecord latest, GroupBy groupBy) {
final train = record.train.trim(); final train = record.train.trim();
final loco = record.loco.trim(); final loco = record.loco.trim();
final locoType = record.locoType.trim();
final latestTrain = latest.train.trim(); final latestTrain = latest.train.trim();
final latestLoco = latest.loco.trim(); final latestLoco = latest.loco.trim();
final latestLocoType = latest.locoType.trim();
switch (groupBy) { switch (groupBy) {
case GroupBy.trainOnly: case GroupBy.trainOnly:
@@ -530,77 +585,14 @@ 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) { String _getLocationInfo(TrainRecord record) {
List<String> parts = []; List<String> parts = [];
if (record.route.isNotEmpty && record.route != "<NUL>") if (record.route.isNotEmpty && record.route != "<NUL>") {
parts.add(record.route); parts.add(record.route);
if (record.direction != 0) parts.add(record.direction == 1 ? "" : ""); }
if (record.direction != 0) {
parts.add(record.direction == 1 ? "" : "");
}
if (record.position.isNotEmpty && record.position != "<NUL>") { if (record.position.isNotEmpty && record.position != "<NUL>") {
final position = record.position; final position = record.position;
final cleanPosition = position.endsWith('.') final cleanPosition = position.endsWith('.')
@@ -616,7 +608,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.map((record) => _parsePosition(record.positionInfo)) .map((record) => _parsePosition(record.positionInfo))
.whereType<LatLng>() .whereType<LatLng>()
.toList(); .toList();
if (positions.isEmpty) return const SizedBox.shrink(); if (positions.isEmpty) {
return const SizedBox.shrink();
}
final mapId = records.map((r) => r.uniqueId).join('_'); final mapId = records.map((r) => r.uniqueId).join('_');
final bounds = LatLngBounds.fromPoints(positions); final bounds = LatLngBounds.fromPoints(positions);
@@ -674,12 +668,6 @@ class HistoryScreenState extends State<HistoryScreen> {
]); ]);
} }
double _getDefaultZoom(List<LatLng> positions) {
if (positions.length == 1) return 15.0;
if (positions.length < 10) return 12.0;
return 10.0;
}
Future<void> _requestLocationPermission() async { Future<void> _requestLocationPermission() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
@@ -695,23 +683,30 @@ class HistoryScreenState extends State<HistoryScreen> {
return; return;
} }
setState(() { if (mounted) {
_isLocationPermissionGranted = true; setState(() {
}); _isLocationPermissionGranted = true;
});
}
_getCurrentLocation(); _getCurrentLocation();
} }
Future<void> _getCurrentLocation() async { Future<void> _getCurrentLocation() async {
try { try {
final locationSettings = AndroidSettings(
accuracy: LocationAccuracy.high,
forceLocationManager: true,
);
Position position = await Geolocator.getCurrentPosition( Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high, locationSettings: locationSettings,
forceAndroidLocationManager: true,
); );
setState(() { if (mounted) {
_currentUserLocation = LatLng(position.latitude, position.longitude); setState(() {
}); _currentUserLocation = LatLng(position.latitude, position.longitude);
});
}
} catch (e) {} } catch (e) {}
} }
@@ -731,10 +726,8 @@ class HistoryScreenState extends State<HistoryScreen> {
final isExpanded = final isExpanded =
!isSubCard && (_expandedStates[record.uniqueId] ?? false); !isSubCard && (_expandedStates[record.uniqueId] ?? false);
final GlobalKey itemKey = GlobalKey(); return Card(
key: key,
final Widget card = Card(
key: key ?? itemKey,
color: isSelected && _isEditMode color: isSelected && _isEditMode
? const Color(0xFF2E2E2E) ? const Color(0xFF2E2E2E)
: const Color(0xFF1E1E1E), : const Color(0xFF1E1E1E),
@@ -762,7 +755,9 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
}, },
onLongPress: () { onLongPress: () {
if (!_isEditMode) setEditMode(true); if (!_isEditMode) {
setEditMode(true);
}
setState(() { setState(() {
_selectedRecords.add(record.uniqueId); _selectedRecords.add(record.uniqueId);
widget.onSelectionChanged(); widget.onSelectionChanged();
@@ -778,21 +773,6 @@ class HistoryScreenState extends State<HistoryScreen> {
_buildLocoInfo(record), _buildLocoInfo(record),
if (isExpanded) _buildExpandedContent(record), if (isExpanded) _buildExpandedContent(record),
])))); ]))));
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_itemHeightCache <= 0 && itemKey.currentContext != null) {
final RenderBox renderBox =
itemKey.currentContext!.findRenderObject() as RenderBox;
final double realHeight = renderBox.size.height;
if (realHeight > 0) {
setState(() {
_itemHeightCache = realHeight;
});
}
}
});
return card;
} }
Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) { Widget _buildRecordHeader(TrainRecord record, {bool isMerged = false}) {
@@ -885,7 +865,9 @@ class HistoryScreenState extends State<HistoryScreen> {
Widget _buildLocoInfo(TrainRecord record) { Widget _buildLocoInfo(TrainRecord record) {
final locoInfo = record.locoInfo; final locoInfo = record.locoInfo;
if (locoInfo == null || locoInfo.isEmpty) return const SizedBox.shrink(); if (locoInfo == null || locoInfo.isEmpty) {
return const SizedBox.shrink();
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text(locoInfo, Text(locoInfo,
@@ -910,8 +892,9 @@ class HistoryScreenState extends State<HistoryScreen> {
.every((r) => r == '*'.runes.first || r == '-'.runes.first) && .every((r) => r == '*'.runes.first || r == '-'.runes.first) &&
speed != "NUL" && speed != "NUL" &&
speed != "<NUL>"; speed != "<NUL>";
if (!isValidRoute && !isValidPosition && !isValidSpeed) if (!isValidRoute && !isValidPosition && !isValidSpeed) {
return const SizedBox.shrink(); return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.only(top: 4.0),
child: child:
@@ -1002,8 +985,11 @@ class HistoryScreenState extends State<HistoryScreen> {
} }
LatLng? _parsePosition(String? positionInfo) { LatLng? _parsePosition(String? positionInfo) {
if (positionInfo == null || positionInfo.isEmpty || positionInfo == '<NUL>') if (positionInfo == null ||
positionInfo.isEmpty ||
positionInfo == '<NUL>') {
return null; return null;
}
try { try {
final parts = positionInfo.trim().split(RegExp(r'\s+')); final parts = positionInfo.trim().split(RegExp(r'\s+'));
if (parts.length >= 2) { if (parts.length >= 2) {
@@ -1022,14 +1008,22 @@ class HistoryScreenState extends State<HistoryScreen> {
double? _parseDmsCoordinate(String dmsStr) { double? _parseDmsCoordinate(String dmsStr) {
try { try {
final degreeIndex = dmsStr.indexOf('°'); final degreeIndex = dmsStr.indexOf('°');
if (degreeIndex == -1) return null; if (degreeIndex == -1) {
return null;
}
final degrees = double.tryParse(dmsStr.substring(0, degreeIndex)); final degrees = double.tryParse(dmsStr.substring(0, degreeIndex));
if (degrees == null) return null; if (degrees == null) {
return null;
}
final minuteIndex = dmsStr.indexOf(''); final minuteIndex = dmsStr.indexOf('');
if (minuteIndex == -1) return degrees; if (minuteIndex == -1) {
return degrees;
}
final minutes = final minutes =
double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex)); double.tryParse(dmsStr.substring(degreeIndex + 1, minuteIndex));
if (minutes == null) return degrees; if (minutes == null) {
return degrees;
}
return degrees + (minutes / 60.0); return degrees + (minutes / 60.0);
} catch (e) { } catch (e) {
return null; return null;
@@ -1140,12 +1134,12 @@ class _DelayedMapWithMarker extends StatefulWidget {
final LatLng? currentUserLocation; final LatLng? currentUserLocation;
const _DelayedMapWithMarker({ const _DelayedMapWithMarker({
Key? key, super.key,
required this.position, required this.position,
required this.zoom, required this.zoom,
required this.recordId, required this.recordId,
this.currentUserLocation, this.currentUserLocation,
}) : super(key: key); });
@override @override
State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState(); State<_DelayedMapWithMarker> createState() => _DelayedMapWithMarkerState();
@@ -1175,13 +1169,17 @@ class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
_mapController.rotate(savedState.bearing); _mapController.rotate(savedState.bearing);
} }
} }
setState(() { if (mounted) {
_isInitializing = false; setState(() {
}); _isInitializing = false;
});
}
} }
void _onCameraMove() { void _onCameraMove() {
if (_isInitializing) return; if (_isInitializing) {
return;
}
final camera = _mapController.camera; final camera = _mapController.camera;
final state = MapState( final state = MapState(
@@ -1280,13 +1278,13 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
final LatLng? currentUserLocation; final LatLng? currentUserLocation;
const _DelayedMultiMarkerMap({ const _DelayedMultiMarkerMap({
Key? key, super.key,
required this.positions, required this.positions,
required this.center, required this.center,
required this.zoom, required this.zoom,
required this.groupKey, required this.groupKey,
this.currentUserLocation, this.currentUserLocation,
}) : super(key: key); });
@override @override
State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState(); State<_DelayedMultiMarkerMap> createState() => _DelayedMultiMarkerMapState();
@@ -1318,13 +1316,17 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
} else if (mounted) { } else if (mounted) {
_mapController.move(widget.center, widget.zoom); _mapController.move(widget.center, widget.zoom);
} }
setState(() { if (mounted) {
_isInitializing = false; setState(() {
}); _isInitializing = false;
});
}
} }
void _onCameraMove() { void _onCameraMove() {
if (_isInitializing) return; if (_isInitializing) {
return;
}
final camera = _mapController.camera; final camera = _mapController.camera;
final state = MapState( final state = MapState(
@@ -1352,7 +1354,7 @@ class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
height: 24, height: 24,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8), color: Colors.red.withAlpha((255 * 0.8).round()),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5)), border: Border.all(color: Colors.white, width: 1.5)),
child: const Icon(Icons.train, color: Colors.white, size: 12)))), child: const Icon(Icons.train, color: Colors.white, size: 12)))),

View File

@@ -32,13 +32,41 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
double _currentRotation = 0.0; double _currentRotation = 0.0;
LatLng? _currentLocation; LatLng? _currentLocation;
LatLng? _lastTrainLocation; LatLng? _lastTrainLocation;
bool _isDataLoaded = false;
final Completer<void> _webViewReadyCompleter = Completer<void>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_initializeWebView(); _initializeWebView();
_initializeServices(); _startInitialization();
}
Future<void> _startInitialization() async {
setState(() {
_isLoading = true;
});
try {
await _loadSettings();
await _loadTrainRecordsFromDatabase();
await _webViewReadyCompleter.future;
_initializeMapCamera();
_initializeLocation();
_startAutoRefresh();
} catch (e, s) {
print('[Flutter] Init Map WebView Screen Failed: $e\n$s');
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
} }
@override @override
@@ -48,16 +76,7 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} }
} }
Future<void> _initializeServices() async { Future<void> _initializeWebView() async {
try {
await _loadSettings();
await _loadTrainRecordsFromDatabase();
_initializeLocation();
_startAutoRefresh();
} catch (e) {}
}
void _initializeWebView() {
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0xFF121212)) ..setBackgroundColor(const Color(0xFF121212))
@@ -92,9 +111,8 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
_isLoading = false; _isLoading = false;
}); });
if (mounted) { if (!_webViewReadyCompleter.isCompleted) {
_initializeMap(); _webViewReadyCompleter.complete();
_initializeLocation();
} }
}, },
onWebResourceError: (WebResourceError error) { onWebResourceError: (WebResourceError error) {
@@ -107,6 +125,8 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
..loadFlutterAsset('assets/mapbox_map.html') ..loadFlutterAsset('assets/mapbox_map.html')
.then((_) {}) .then((_) {})
.catchError((error) {}); .catchError((error) {});
await Future.delayed(const Duration(milliseconds: 500));
} }
Future<void> _initializeLocation() async { Future<void> _initializeLocation() async {
@@ -266,10 +286,6 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} else {} } else {}
} else {} } else {}
} else {} } else {}
if (mounted) {
_initializeMapPosition();
}
}); });
Future.delayed(const Duration(milliseconds: 3000), () { Future.delayed(const Duration(milliseconds: 3000), () {
@@ -351,25 +367,38 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} }
} }
void _initializeMapPosition() { void _initializeMapCamera() {
if (_isMapInitialized) return; if (_isMapInitialized) return;
LatLng? targetLocation; LatLng targetLocation;
double targetZoom = _currentZoom;
double targetRotation = _currentRotation;
if (_lastTrainLocation != null) { if (_currentLocation != null) {
targetLocation = _lastTrainLocation; targetLocation = _currentLocation!;
} else if (_currentPosition != null) { } else if (_lastTrainLocation != null) {
targetLocation = targetLocation = _lastTrainLocation!;
LatLng(_currentPosition!.latitude, _currentPosition!.longitude); targetZoom = 14.0;
} targetRotation = 0.0;
if (targetLocation != null) {
_centerMap(targetLocation,
zoom: _currentZoom, rotation: _currentRotation);
_isMapInitialized = true;
} else { } else {
_isMapInitialized = true; targetLocation = const LatLng(39.9042, 116.4074);
targetZoom = 10.0;
} }
_centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation);
_isMapInitialized = true;
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_updateUserLocation();
_updateTrainMarkers();
_controller.runJavaScript('''
if (window.MapInterface) {
window.MapInterface.setRailwayVisible($_isRailwayLayerVisible);
}
''');
}
});
} }
void _centerMap(LatLng location, {double? zoom, double? rotation}) { void _centerMap(LatLng location, {double? zoom, double? rotation}) {
@@ -430,91 +459,6 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
} catch (e) {} } catch (e) {}
} }
Future<void> _initializeMap() async {
try {
LatLng? targetLocation;
double targetZoom = 14.0;
double targetRotation = _currentRotation;
if (_lastTrainLocation != null) {
targetLocation = _lastTrainLocation;
targetZoom = 14.0;
targetRotation = 0.0;
} else if (_currentPosition != null) {
targetLocation =
LatLng(_currentPosition!.latitude, _currentPosition!.longitude);
targetZoom = 16.0;
targetRotation = 0.0;
}
if (targetLocation != null) {
_centerMap(targetLocation, zoom: targetZoom, rotation: targetRotation);
try {
await _controller.runJavaScript('''
(function() {
if (window.map && window.map.getContainer) {
window.map.getContainer().style.opacity = '1';
return true;
} else {
return false;
}
})();
''');
} catch (e) {
print('显示地图失败: $e');
Future.delayed(const Duration(milliseconds: 500), () {
_controller.runJavaScript('''
(function() {
if (window.map && window.map.getContainer) {
window.map.getContainer().style.opacity = '1';
console.log('延迟显示地图成功');
}
})();
''');
});
}
}
setState(() {
_isMapInitialized = true;
});
} catch (e, stackTrace) {}
Future.delayed(const Duration(milliseconds: 1000), () {
if (mounted && _isMapInitialized) {
_updateTrainMarkers();
_controller.runJavaScript('''
(function() {
if (window.MapInterface) {
try {
window.MapInterface.setRailwayVisible($_isRailwayLayerVisible);
console.log('初始化铁路图层状态: $_isRailwayLayerVisible');
} catch (error) {
console.error('初始化铁路图层失败:', error);
}
}
})();
''');
}
});
Future.delayed(const Duration(milliseconds: 3000), () {
if (mounted && _isMapInitialized) {
_updateTrainMarkers();
}
});
Future.delayed(const Duration(seconds: 5), () {
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
});
}
LatLng? _parseDmsCoordinate(String? positionInfo) { LatLng? _parseDmsCoordinate(String? positionInfo) {
if (positionInfo == null || if (positionInfo == null ||
positionInfo.isEmpty || positionInfo.isEmpty ||
@@ -943,10 +887,10 @@ class MapWebViewScreenState extends State<MapWebViewScreen>
void _onMapStateChanged(Map<String, dynamic> mapState) { void _onMapStateChanged(Map<String, dynamic> mapState) {
try { try {
final lat = mapState['lat'] as double; final lat = (mapState['lat'] as num).toDouble();
final lng = mapState['lng'] as double; final lng = (mapState['lng'] as num).toDouble();
final zoom = mapState['zoom'] as double; final zoom = (mapState['zoom'] as num).toDouble();
final bearing = mapState['bearing'] as double; final bearing = (mapState['bearing'] as num).toDouble();
setState(() { setState(() {
_currentLocation = LatLng(lat, lng); _currentLocation = LatLng(lat, lng);

View File

@@ -13,7 +13,7 @@ class DatabaseService {
DatabaseService._internal(); DatabaseService._internal();
static const String _databaseName = 'train_database'; static const String _databaseName = 'train_database';
static const _databaseVersion = 6; static const _databaseVersion = 7;
static const String trainRecordsTable = 'train_records'; static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings'; static const String appSettingsTable = 'app_settings';
@@ -87,6 +87,10 @@ class DatabaseService {
await db.execute( await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0'); 'ALTER TABLE $appSettingsTable ADD COLUMN hideUngroupableRecords INTEGER NOT NULL DEFAULT 0');
} }
if (oldVersion < 7) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN mapSettingsTimestamp INTEGER');
}
} }
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
@@ -136,7 +140,8 @@ class DatabaseService {
groupBy TEXT NOT NULL DEFAULT 'trainAndLoco', groupBy TEXT NOT NULL DEFAULT 'trainAndLoco',
timeWindow TEXT NOT NULL DEFAULT 'unlimited', timeWindow TEXT NOT NULL DEFAULT 'unlimited',
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited', mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
hideUngroupableRecords INTEGER NOT NULL DEFAULT 0 hideUngroupableRecords INTEGER NOT NULL DEFAULT 0,
mapSettingsTimestamp INTEGER
) )
'''); ''');
@@ -164,6 +169,7 @@ class DatabaseService {
'timeWindow': 'unlimited', 'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited', 'mapTimeFilter': 'unlimited',
'hideUngroupableRecords': 0, 'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
}); });
} }

View File

@@ -2,7 +2,7 @@ name: lbjconsole
description: "LBJ Console" description: "LBJ Console"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
@@ -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.5.0-flutter+50 version: 0.5.2-flutter+52
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -76,7 +76,6 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
@@ -129,4 +128,4 @@ msix_config:
publisher: CN=noxylva, O=noxylva.org, C=US publisher: CN=noxylva, O=noxylva.org, C=US
logo_path: assets/icon.png logo_path: assets/icon.png
capabilities: bluetooth,internetClient,location capabilities: bluetooth,internetClient,location
certificate_path: keystore.jks certificate_path: keystore.jks