diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index dad5ed1..86484d8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4753,8 +4753,20 @@ class MeshCoreConnector extends ChangeNotifier { // CRITICAL: Preserve user's path override when contact is refreshed from device _contacts[existingIndex] = existing.copyWith( - latitude: hasLocation ? latitude : existing.latitude, - longitude: hasLocation ? longitude : existing.longitude, + latitude: + hasLocation && + latitude != null && + latitude.abs() <= 90 && + longitude != 0 + ? latitude + : existing.latitude, + longitude: + hasLocation && + longitude != null && + longitude.abs() <= 180 && + longitude != 0 + ? longitude + : existing.longitude, name: hasName ? name : existing.name, path: Uint8List.fromList(path.reversed.toList()), pathLength: path.length, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index cab58cb..858d712 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import '../connector/meshcore_protocol.dart'; @@ -65,7 +66,17 @@ class Contact { return '$pathLength hops'; } - bool get hasLocation => latitude != null && longitude != null; + bool get hasLocation { + const double epsilon = 1e-6; + final lat = latitude ?? 0.0; + final lon = longitude ?? 0.0; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + bool get isFavorite => (flags & contactFlagFavorite) != 0; Contact copyWith({ @@ -108,7 +119,7 @@ class Contact { } String get pathIdList { - final pathBytes = _pathBytesForDisplay; + final pathBytes = pathBytesForDisplay; if (pathBytes.isEmpty) return ''; final parts = []; final groupSize = pathHashSize; @@ -130,43 +141,7 @@ class Contact { return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; } - Uint8List? get traceRouteBytes { - final pathBytes = _pathBytesForDisplay; - Uint8List? traceBytes; - - if (pathBytes.isEmpty) { - traceBytes = Uint8List(1); - traceBytes[0] = publicKey[0]; - return traceBytes; - } - - if (type == advTypeRepeater || type == advTypeRoom) { - final len = (pathBytes.length + pathBytes.length + 1); - traceBytes = Uint8List(len); - traceBytes[pathBytes.length] = publicKey[0]; - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } else { - if (pathBytes.length < 2) { - return pathBytes[0] == 0 ? null : pathBytes; - } - final len = (pathBytes.length + pathBytes.length - 1); - traceBytes = Uint8List(len); - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length - 1) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } - return traceBytes; - } - - Uint8List get _pathBytesForDisplay { + Uint8List get pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); return pathOverrideBytes ?? Uint8List(0); @@ -197,6 +172,7 @@ class Contact { double? lat, lon; final latRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE(); + if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 32eadef..747c2bf 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -62,8 +62,9 @@ class ChannelMessagePathScreen extends StatelessWidget { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: primaryPath, - flipPathRound: true, - reversePathRound: !message.isOutgoing && !channelMessage, + flipPathAround: true, + reversePathAround: + !(!channelMessage && !message.isOutgoing), ), ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 6558ecd..5209b41 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -858,7 +858,7 @@ class _ChatScreenState extends State { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 243c8c4..ed2e171 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1064,7 +1064,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { @@ -1072,10 +1072,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, + targetContact: contact, ), ), ); @@ -1100,10 +1102,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: contact.pathBytesForDisplay.isNotEmpty, + targetContact: contact, ), ), ); @@ -1145,7 +1149,8 @@ class _ContactsScreenState extends State title: context.l10n.contacts_pathTraceTo( contact.name, ), - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, targetContact: contact, ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 497c05f..df16a59 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -176,20 +176,13 @@ class _MapScreenState extends State { // Filter by location final contactsWithLocation = filteredByKeyPrefix.where((c) { - if (!c.hasLocation) { - return false; - } - return _checkLocationPlausibility(c.latitude!, c.longitude!); + return c.hasLocation; }).toList(); // All contacts with a known location — used as anchors regardless of // time/key-prefix filters so that repeaters are always available. final allContactsWithLocation = allContacts - .where( - (c) => - c.hasLocation && - _checkLocationPlausibility(c.latitude!, c.longitude!), - ) + .where((c) => c.hasLocation) .toList(); // Compute guessed locations with caching diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 6277886..d50a185 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -52,8 +52,8 @@ class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; final int? repeaterId; - final bool flipPathRound; - final bool reversePathRound; + final bool flipPathAround; + final bool reversePathAround; final Contact? targetContact; const PathTraceMapScreen({ @@ -61,8 +61,8 @@ class PathTraceMapScreen extends StatefulWidget { required this.title, required this.path, this.repeaterId, - this.flipPathRound = false, - this.reversePathRound = false, + this.flipPathAround = false, + this.reversePathAround = false, this.targetContact, }); @@ -93,6 +93,7 @@ class _PathTraceMapScreenState extends State { ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; + Contact? target; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -158,21 +159,16 @@ class _PathTraceMapScreenState extends State { }); } - final Uint8List path; - - Uint8List pathTmp = widget.reversePathRound + final pathTmp = widget.reversePathAround ? Uint8List.fromList(widget.path.reversed.toList()) : widget.path; - if (widget.flipPathRound) { - path = buildPath(pathTmp); - } else { - path = pathTmp; - } + final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; appLogger.info( 'Initiating path trace with path: ${_formatPathPrefixes(path)}', tag: 'PathTraceMapScreen', + noNotify: !mounted, ); final connector = Provider.of(context, listen: false); @@ -309,18 +305,20 @@ class _PathTraceMapScreenState extends State { // Compute endpoint position for the target contact. LatLng? targetPos; bool targetGuessed = false; - final target = widget.targetContact; + target = widget.targetContact; + if (target != null) { - if (target.hasLocation) { - targetPos = LatLng(target.latitude!, target.longitude!); - } else if (pathData.isNotEmpty) { + if (target?.hasLocation ?? false) { + targetPos = LatLng(target!.latitude!, target!.longitude!); + } else if (widget.path.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. // For a round-trip path (flipPathRound), the target-side hop sits // in the middle of the symmetric sequence; .last is the local side. - final lastHop = (widget.flipPathRound && pathData.length > 1) - ? pathData[(pathData.length - 1) ~/ 2] - : pathData.last; - final peers = connector.contacts + final lastHop = widget.reversePathAround + ? widget.path.first + : widget.path.last; + + final peers = connector.allContacts .where( (c) => c.hasLocation && @@ -336,12 +334,35 @@ class _PathTraceMapScreenState extends State { peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; const offsetDeg = 0.003; - final angle = (target.publicKey[1] / 255.0) * 2 * pi; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( lat + offsetDeg * cos(angle), lon + offsetDeg * sin(angle), ); targetGuessed = true; + } else if (inferredPositions.containsKey(lastHop)) { + final lat = inferredPositions[lastHop]!.latitude; + final lon = inferredPositions[lastHop]!.longitude; + const offsetDeg = 0.003; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } else { + // As a last resort, just place it at the same position as the last hop. + final contact = pathContacts[lastHop]; + if (contact != null && contact.hasLocation) { + const offsetDeg = 0.003; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + contact.latitude! + offsetDeg * cos(angle), + contact.longitude! + offsetDeg * sin(angle), + ); + targetGuessed = true; + targetGuessed = true; + } } } } @@ -350,7 +371,12 @@ class _PathTraceMapScreenState extends State { _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + int hopLast = 0; + int hopLastLast = 0; for (final hop in _traceData!.pathData) { + if (hop == hopLastLast && widget.flipPathAround) { + break; //skip duplicate hops in round-trip paths + } final contact = _traceData!.pathContacts[hop]; if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); @@ -358,8 +384,14 @@ class _PathTraceMapScreenState extends State { final inferred = inferredPositions[hop]; if (inferred != null) _points.add(inferred); } + hopLastLast = hopLast; + hopLast = hop; + } + if (targetPos != null) { + if (target != null && target!.type == advTypeChat) { + _points.add(targetPos); + } } - if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -448,7 +480,7 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) _buildMapPathTrace(context, tileCache), + if (_hasData) _buildMapPathTrace(context, tileCache, target), if (_points.isEmpty && !_hasData && !_isLoading && @@ -477,17 +509,28 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers( List pathData, { required bool showLabels, + required Contact? target, }) { final markers = []; + int hopLast = 0; + int hopLastLast = 0; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; final inferred = _inferredHopPositions[hop]; final hasGps = contact != null && contact.hasLocation; - if (!hasGps && inferred == null) continue; + if (hop == hopLastLast && widget.flipPathAround) { + continue; //skip duplicate hops in round-trip paths + } + if (!hasGps && inferred == null) { + hopLastLast = hopLast; + hopLast = hop; + continue; //skip hops with no GPS and no inferred position + } final point = hasGps ? LatLng(contact.latitude!, contact.longitude!) : inferred!; final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + markers.add( Marker( point: point, @@ -529,6 +572,8 @@ class _PathTraceMapScreenState extends State { ), ); } + hopLastLast = hopLast; + hopLast = hop; } final selfLat = context.read().selfLatitude; @@ -578,9 +623,9 @@ class _PathTraceMapScreenState extends State { // Add target contact endpoint marker. final targetPos = _targetContactPosition; - if (targetPos != null) { + if (targetPos != null && target != null && target.type == advTypeChat) { final isGuessed = _targetContactIsGuessed; - final targetName = widget.targetContact?.name ?? '?'; + final targetName = target.name; markers.add( Marker( point: targetPos, @@ -716,6 +761,7 @@ class _PathTraceMapScreenState extends State { Widget _buildMapPathTrace( BuildContext context, MapTileCacheService tileCache, + Contact? target, ) { return FlutterMap( key: _mapKey, @@ -754,6 +800,7 @@ class _PathTraceMapScreenState extends State { markers: _buildHopMarkers( _traceData!.pathData, showLabels: _showNodeLabels, + target: target, ), ), ], diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index c63e625..d31c3e5 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -51,6 +51,7 @@ class AppDebugLogService extends ChangeNotifier { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (!_enabled && !kDebugMode) return; if (!_enabled) { @@ -72,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier { _entries.removeRange(0, _entries.length - maxEntries); } - notifyListeners(); + if (!noNotify) { + notifyListeners(); + } // Also print to console for development debugPrint('[$tag] $message'); } - void info(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.info); + void info(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify); } - void warn(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.warning); + void warn(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify); } - void error(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.error); + void error(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify); } void clear() { diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart index e57261e..1f34a5e 100644 --- a/lib/utils/app_logger.dart +++ b/lib/utils/app_logger.dart @@ -23,23 +23,23 @@ class AppLogger { bool get isEnabled => _enabled; /// Log an info message - void info(String message, {String tag = 'App'}) { + void info(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.info(message, tag: tag); + _service!.info(message, tag: tag, noNotify: noNotify); } } /// Log a warning message - void warn(String message, {String tag = 'App'}) { + void warn(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.warn(message, tag: tag); + _service!.warn(message, tag: tag, noNotify: noNotify); } } /// Log an error message - void error(String message, {String tag = 'App'}) { + void error(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.error(message, tag: tag); + _service!.error(message, tag: tag, noNotify: noNotify); } } @@ -48,9 +48,10 @@ class AppLogger { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (_enabled && _service != null) { - _service!.log(message, tag: tag, level: level); + _service!.log(message, tag: tag, level: level, noNotify: noNotify); } } } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 0233b43..861241b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -78,7 +78,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ),