mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 08:46:40 +10:00
Enhance location handling and UI updates
- Refactor location permission handling in SparseLocationLogger - Add updateMyLocation method to SparseLocationLogger - Introduce new contact handling in MeshCoreConnector - Update map screen with a new option to refresh location - Add localization for "Update Location" in app_en.arb - Adjust payload types in meshcore_protocol.dart
This commit is contained in:
@@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
import 'package:geolocator_platform_interface/src/models/position.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||||
import 'package:pointycastle/export.dart';
|
import 'package:pointycastle/export.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -1699,6 +1699,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_isLoadingContacts = true;
|
_isLoadingContacts = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
|
case pushCodeNewAdvert:
|
||||||
|
debugPrint('Got New CONTACT');
|
||||||
|
// It the same format as respCodeContact, so we can reuse the handler
|
||||||
|
_handleContact(frame);
|
||||||
|
break;
|
||||||
case respCodeContact:
|
case respCodeContact:
|
||||||
debugPrint('Got CONTACT');
|
debugPrint('Got CONTACT');
|
||||||
_handleContact(frame);
|
_handleContact(frame);
|
||||||
@@ -1743,6 +1748,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
case pushCodeStatusResponse:
|
case pushCodeStatusResponse:
|
||||||
break;
|
break;
|
||||||
case pushCodeLogRxData:
|
case pushCodeLogRxData:
|
||||||
|
_handleRxData(frame);
|
||||||
_handleLogRxData(frame);
|
_handleLogRxData(frame);
|
||||||
break;
|
break;
|
||||||
case respCodeChannelInfo:
|
case respCodeChannelInfo:
|
||||||
@@ -1756,6 +1762,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case respCodeCustomVars:
|
case respCodeCustomVars:
|
||||||
_handleCustomVars(frame);
|
_handleCustomVars(frame);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
debugPrint('Unknown frame code: $code');
|
debugPrint('Unknown frame code: $code');
|
||||||
}
|
}
|
||||||
@@ -2011,6 +2018,76 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleContactAdvert(Contact contact) {
|
||||||
|
if (contact.type == advTypeRepeater) {
|
||||||
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex);
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt =
|
||||||
|
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||||
|
? existing.lastMessageAt
|
||||||
|
: contact.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
|
_contacts[existingIndex] = contact.copyWith(
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_contacts.add(contact);
|
||||||
|
appLogger.info(
|
||||||
|
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_knownContactKeys.add(contact.publicKeyHex);
|
||||||
|
_loadMessagesForContact(contact.publicKeyHex);
|
||||||
|
|
||||||
|
// Add path to history if we have a valid path
|
||||||
|
if (_pathHistoryService != null && contact.pathLength >= 0) {
|
||||||
|
_pathHistoryService!.handlePathUpdated(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Show notification for new contact (advertisement)
|
||||||
|
if (isNewContact && _appSettingsService != null) {
|
||||||
|
final settings = _appSettingsService!.settings;
|
||||||
|
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||||
|
_notificationService.showAdvertNotification(
|
||||||
|
contactName: contact.name,
|
||||||
|
contactType: contact.typeLabel,
|
||||||
|
contactId: contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isLoadingContacts) {
|
||||||
|
unawaited(_persistContacts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistContacts() async {
|
Future<void> _persistContacts() async {
|
||||||
await _contactStore.saveContacts(_contacts);
|
await _contactStore.saveContacts(_contacts);
|
||||||
}
|
}
|
||||||
@@ -3296,20 +3373,133 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateLocationandAdvert(Position position) async {
|
_updateLocationandAdvert(Position position) async {
|
||||||
double lat = position.latitude;
|
final snapToGridCenter = _sparseLocationLogger?.snapToGridCenter(
|
||||||
double lon = position.longitude;
|
position: position,
|
||||||
|
cellSizeMeters: 0.001,
|
||||||
|
);
|
||||||
|
double lat = snapToGridCenter?.latitude ?? 0.0;
|
||||||
|
double lon = snapToGridCenter?.longitude ?? 0.0;
|
||||||
|
|
||||||
if (lat == 0.0 && lon == 0.0) {
|
if (lat == 0.0 && lon == 0.0) {
|
||||||
// Invalid location
|
debugPrint('Invalid location (0,0), skipping advert');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lat = double.parse(lat.toStringAsFixed(3)) - 0.00015;
|
|
||||||
lon = double.parse(lon.toStringAsFixed(3)) - 0.00015;
|
await sendFrame(buildSetOtherParamsFrame(true, 1, 1, 0));
|
||||||
print('Updating location to lat: $lat, lon: $lon');
|
|
||||||
await sendFrame(buildSetOtherParamsFrame(true, 0, 1, 0));
|
|
||||||
await setNodeLocation(lat: lat, lon: lon);
|
await setNodeLocation(lat: lat, lon: lon);
|
||||||
await sendSelfAdvert(flood: true);
|
await sendSelfAdvert(flood: true);
|
||||||
await sendFrame(buildDeviceQueryFrame());
|
_selfLatitude = lat;
|
||||||
|
_selfLongitude = lon;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRxData(Uint8List frame) {
|
||||||
|
final packet = BufferReader(frame);
|
||||||
|
packet.skipBytes(3); // Skip frame type byte
|
||||||
|
//final snr = packet.readByte() / 4.0;
|
||||||
|
//final rssi = packet.readByte();
|
||||||
|
final header = packet.readByte();
|
||||||
|
//final routeType = header & 0x03;
|
||||||
|
final payloadType = (header >> 2) & 0x0F;
|
||||||
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
|
|
||||||
|
if (packet.remaining <= 0) return;
|
||||||
|
final pathLen = packet.readByte();
|
||||||
|
|
||||||
|
if (packet.remaining < pathLen) return;
|
||||||
|
final pathBytes = packet.readBytes(pathLen);
|
||||||
|
|
||||||
|
if (packet.remaining <= 0) return;
|
||||||
|
final payload = packet.readBytes(packet.remaining);
|
||||||
|
|
||||||
|
switch (payloadType) {
|
||||||
|
case payloadTypeADVERT:
|
||||||
|
_handlePayloadAdvertReceived(payload, pathBytes);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePayloadAdvertReceived(Uint8List frame, Uint8List path) {
|
||||||
|
final advert = BufferReader(frame);
|
||||||
|
if (advert.remaining <= 32) return;
|
||||||
|
final publicKey = advert.readBytes(32);
|
||||||
|
final contactKeyHex = publicKey
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
if (advert.remaining <= 4) return;
|
||||||
|
final timestamp = advert.readInt32LE();
|
||||||
|
if (advert.remaining <= 64) return;
|
||||||
|
advert.skipBytes(64); // Skip signature for now
|
||||||
|
if (advert.remaining <= 1) return;
|
||||||
|
final flags = advert.readByte();
|
||||||
|
final type = flags & 0x0F;
|
||||||
|
final hasLocation = (flags & 0x10) != 0;
|
||||||
|
//final hasFeature1 = (flags & 0x20) != 0;
|
||||||
|
//final hasFeature2 = (flags & 0x40) != 0;
|
||||||
|
final hasName = (flags & 0x80) != 0;
|
||||||
|
double latitude = 0.0;
|
||||||
|
double longitude = 0.0;
|
||||||
|
if (hasLocation && advert.remaining >= 8) {
|
||||||
|
latitude = advert.readInt32LE() / 1e6;
|
||||||
|
longitude = advert.readInt32LE() / 1e6;
|
||||||
|
}
|
||||||
|
String name = '';
|
||||||
|
if (hasName && advert.remaining > 0) {
|
||||||
|
name = advert.readString();
|
||||||
|
}
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contactKeyHex);
|
||||||
|
if (isNewContact) {
|
||||||
|
_handleContactAdvert(
|
||||||
|
Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
pathLength: path.length,
|
||||||
|
path: path,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contactKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now())
|
||||||
|
? DateTime.now()
|
||||||
|
: existing.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
name: hasName ? name : existing.name,
|
||||||
|
path: path,
|
||||||
|
pathLength: path.length,
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,30 @@ const int advTypeRepeater = 2;
|
|||||||
const int advTypeRoom = 3;
|
const int advTypeRoom = 3;
|
||||||
const int advTypeSensor = 4;
|
const int advTypeSensor = 4;
|
||||||
|
|
||||||
|
// Payload Types
|
||||||
|
const int payloadTypeREQ =
|
||||||
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeRESPONSE =
|
||||||
|
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeTXTMSG =
|
||||||
|
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||||
|
const int payloadTypeACK = 0x03; // a simple ack
|
||||||
|
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||||
|
const int payloadTypeGRPTXT =
|
||||||
|
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||||
|
const int payloadTypeGRPDATA =
|
||||||
|
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeANONREQ =
|
||||||
|
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||||
|
const int payloadTypePATH =
|
||||||
|
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||||
|
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||||
|
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||||
|
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||||
|
//...
|
||||||
|
const int payloadTypeRawCustom =
|
||||||
|
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
const int pubKeySize = 32;
|
const int pubKeySize = 32;
|
||||||
const int maxPathSize = 64;
|
const int maxPathSize = 64;
|
||||||
@@ -780,20 +804,20 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_SET_OTHER_PARAMS frame
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advert_loc_policy][multi_acks]
|
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
Uint8List buildSetOtherParamsFrame(
|
Uint8List buildSetOtherParamsFrame(
|
||||||
bool allowAutoAddContacts,
|
bool allowAutoAddContacts,
|
||||||
int allowTelemetryFlags,
|
int allowTelemetryFlags,
|
||||||
int advert_loc_policy,
|
int advertLocationPolicy,
|
||||||
int multi_acks,
|
int multiAcks,
|
||||||
) {
|
) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSetOtherParams);
|
writer.writeByte(cmdSetOtherParams);
|
||||||
writer.writeByte(
|
writer.writeByte(
|
||||||
allowAutoAddContacts ? 0x01 : 0x00,
|
allowAutoAddContacts ? 0x00 : 0x01,
|
||||||
); // Allow Auto Add Contacts
|
); // Allow Auto Add Contacts
|
||||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
writer.writeByte(advert_loc_policy); // Advertisement Location Policy
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
writer.writeByte(multi_acks); // Multi Acknowledgements
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -619,6 +619,7 @@
|
|||||||
"map_sharedPin": "Shared pin",
|
"map_sharedPin": "Shared pin",
|
||||||
"map_joinRoom": "Join Room",
|
"map_joinRoom": "Join Room",
|
||||||
"map_manageRepeater": "Manage Repeater",
|
"map_manageRepeater": "Manage Repeater",
|
||||||
|
"map_updateMyLocation": "Update Location",
|
||||||
"mapCache_title": "Offline Map Cache",
|
"mapCache_title": "Offline Map Cache",
|
||||||
"mapCache_selectAreaFirst": "Select an area to cache first",
|
"mapCache_selectAreaFirst": "Select an area to cache first",
|
||||||
"mapCache_noTilesToDownload": "No tiles to download for this area",
|
"mapCache_noTilesToDownload": "No tiles to download for this area",
|
||||||
|
|||||||
@@ -275,6 +275,17 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.refresh),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.map_updateMyLocation),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
connector.sparseLocationLogger?.updateMyLocation(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:meshcore_open/utils/gpx_export.dart';
|
|||||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:gpx/gpx.dart';
|
import 'package:gpx/gpx.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -34,17 +35,24 @@ class SparseLocationLogger {
|
|||||||
_onNewLogPoint = onNewLogPoint;
|
_onNewLogPoint = onNewLogPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startLogging() async {
|
Future<bool> getPremissons() async {
|
||||||
// Permissions & service check (same as before)
|
// Permissions & service check (same as before)
|
||||||
var status = await Permission.location.request();
|
var status = await Permission.location.request();
|
||||||
if (!status.isGranted) {
|
if (!status.isGranted) {
|
||||||
print('Location permission denied');
|
debugPrint('Location permission denied');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
print('Location services disabled');
|
debugPrint('Location services disabled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startLogging() async {
|
||||||
|
if (!await getPremissons()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +100,7 @@ class SparseLocationLogger {
|
|||||||
_lastHeading = null;
|
_lastHeading = null;
|
||||||
_lastLoggedTime = null;
|
_lastLoggedTime = null;
|
||||||
|
|
||||||
print('Sparse GPX logging started → ${_gpxFile?.path}');
|
debugPrint('Sparse GPX logging started → ${_gpxFile?.path}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopLogging() async {
|
Future<void> stopLogging() async {
|
||||||
@@ -107,7 +115,7 @@ class SparseLocationLogger {
|
|||||||
|
|
||||||
await _gpxFile?.writeAsString(xmlString);
|
await _gpxFile?.writeAsString(xmlString);
|
||||||
|
|
||||||
final result = await SharePlus.instance.share(
|
await SharePlus.instance.share(
|
||||||
ShareParams(
|
ShareParams(
|
||||||
text: 'Sparse GPS track',
|
text: 'Sparse GPS track',
|
||||||
subject: 'Sparse GPS track',
|
subject: 'Sparse GPS track',
|
||||||
@@ -118,7 +126,20 @@ class SparseLocationLogger {
|
|||||||
await _gpxFile?.delete();
|
await _gpxFile?.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Logging stopped');
|
debugPrint('Logging stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateMyLocation() async {
|
||||||
|
if (!await getPremissons()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final position = await Geolocator.getCurrentPosition();
|
||||||
|
_onNewLogPoint?.call(position);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error updating location: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPositionReceived(Position position) async {
|
Future<void> _onPositionReceived(Position position) async {
|
||||||
@@ -176,30 +197,30 @@ class SparseLocationLogger {
|
|||||||
|
|
||||||
_currentSegment.trkpts.add(pt);
|
_currentSegment.trkpts.add(pt);
|
||||||
_onNewLogPoint?.call(position);
|
_onNewLogPoint?.call(position);
|
||||||
print('Logged point: ${pt.lat}, ${pt.lon} ($reason)');
|
debugPrint('Logged point: ${pt.lat}, ${pt.lon} ($reason)');
|
||||||
|
|
||||||
_lastLoggedPosition = position;
|
_lastLoggedPosition = position;
|
||||||
_lastHeading = heading;
|
_lastHeading = heading;
|
||||||
_lastLoggedTime = now;
|
_lastLoggedTime = now;
|
||||||
} else {
|
} else {
|
||||||
print('Skipped point: ${position.latitude}, ${position.longitude}');
|
debugPrint('Skipped point: ${position.latitude}, ${position.longitude}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Position snapToGridCenter({
|
Position snapToGridCenter({
|
||||||
required Position position,
|
required Position position,
|
||||||
required double cellSizeDegrees, // e.g. 0.01 ≈ 1.1 km, 0.001 ≈ 110 m
|
required double cellSizeMeters,
|
||||||
}) {
|
}) {
|
||||||
Position snappedPosition = position;
|
Position snappedPosition = position;
|
||||||
// Snap latitude
|
// Snap latitude
|
||||||
final latFloor =
|
final latFloor =
|
||||||
(position.latitude / cellSizeDegrees).floor() * cellSizeDegrees;
|
(position.latitude / cellSizeMeters).floor() * cellSizeMeters;
|
||||||
final snappedLat = latFloor + (cellSizeDegrees / 2);
|
final snappedLat = latFloor + (cellSizeMeters / 2);
|
||||||
|
|
||||||
// Snap longitude
|
// Snap longitude
|
||||||
final lonFloor =
|
final lonFloor =
|
||||||
(position.longitude / cellSizeDegrees).floor() * cellSizeDegrees;
|
(position.longitude / cellSizeMeters).floor() * cellSizeMeters;
|
||||||
final snappedLon = lonFloor + (cellSizeDegrees / 2);
|
final snappedLon = lonFloor + (cellSizeMeters / 2);
|
||||||
|
|
||||||
snappedPosition = Position(
|
snappedPosition = Position(
|
||||||
latitude: snappedLat,
|
latitude: snappedLat,
|
||||||
|
|||||||
Reference in New Issue
Block a user