|
|
|
|
@ -1,3 +1,4 @@
|
|
|
|
|
import 'dart:collection';
|
|
|
|
|
import 'dart:math';
|
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
|
|
@ -52,7 +53,7 @@ class MapScreen extends StatefulWidget {
|
|
|
|
|
|
|
|
|
|
class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
// Zoom level at which node labels start to appear
|
|
|
|
|
static const double _labelZoomThreshold = 12.0;
|
|
|
|
|
static const double _labelZoomThreshold = 14.0;
|
|
|
|
|
|
|
|
|
|
final MapController _mapController = MapController();
|
|
|
|
|
final MapMarkerService _markerService = MapMarkerService();
|
|
|
|
|
@ -329,7 +330,9 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
if (!_isBuildingPathTrace)
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.radar),
|
|
|
|
|
onPressed: () => _startPath(),
|
|
|
|
|
onPressed: () => _startPath(
|
|
|
|
|
LatLng(connector.selfLatitude!, connector.selfLongitude!),
|
|
|
|
|
),
|
|
|
|
|
tooltip: context.l10n.contacts_pathTrace,
|
|
|
|
|
),
|
|
|
|
|
if (!_isBuildingPathTrace)
|
|
|
|
|
@ -580,6 +583,7 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
// Index known-location repeaters by their 1-byte hash.
|
|
|
|
|
// null value = two repeaters share the same hash byte (ambiguous collision).
|
|
|
|
|
final repeaterByHash = <int, Contact?>{};
|
|
|
|
|
|
|
|
|
|
for (final c in withLocation) {
|
|
|
|
|
if (c.type == advTypeRepeater) {
|
|
|
|
|
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
|
|
|
|
@ -595,6 +599,11 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
|
|
|
|
|
for (final contact in allContacts) {
|
|
|
|
|
if (contact.hasLocation) continue;
|
|
|
|
|
if (contact.lastSeen.isBefore(
|
|
|
|
|
DateTime.now().subtract(const Duration(hours: 24)),
|
|
|
|
|
)) {
|
|
|
|
|
continue; // skip stale contacts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final anchorSet = <LatLng>{};
|
|
|
|
|
|
|
|
|
|
@ -641,10 +650,19 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
continue; // discard implausible guesses near (0, 0)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
double lat = 0, lon = 0;
|
|
|
|
|
double lat = 0, lon = 0, weight = 1.0;
|
|
|
|
|
int counted = 0;
|
|
|
|
|
for (final a in anchors) {
|
|
|
|
|
lat += a.latitude;
|
|
|
|
|
lon += a.longitude;
|
|
|
|
|
if (counted == 0) {
|
|
|
|
|
lat = a.latitude;
|
|
|
|
|
lon = a.longitude;
|
|
|
|
|
} else {
|
|
|
|
|
lat += a.latitude * weight;
|
|
|
|
|
lon += a.longitude * weight;
|
|
|
|
|
}
|
|
|
|
|
// weight subsequent anchors less to create a bias towards the first (if more than 2)
|
|
|
|
|
weight = weight / 2;
|
|
|
|
|
counted++;
|
|
|
|
|
}
|
|
|
|
|
position = _offsetGuessedPosition(
|
|
|
|
|
LatLng(lat / anchors.length, lon / anchors.length),
|
|
|
|
|
@ -812,31 +830,67 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
return markers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Marker> _buildMarkers(
|
|
|
|
|
List<Contact> _filterContactsBySettings(
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
settings, {
|
|
|
|
|
required bool showLabels,
|
|
|
|
|
}) {
|
|
|
|
|
final markers = <Marker>[];
|
|
|
|
|
|
|
|
|
|
dynamic settings,
|
|
|
|
|
) {
|
|
|
|
|
List<Contact> filtered = [];
|
|
|
|
|
bool addContact = false;
|
|
|
|
|
for (final contact in contacts) {
|
|
|
|
|
addContact = false;
|
|
|
|
|
if (!contact.hasLocation) continue;
|
|
|
|
|
|
|
|
|
|
// Apply node type filters
|
|
|
|
|
if (contact.type == advTypeRepeater &&
|
|
|
|
|
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
|
|
|
|
|
continue;
|
|
|
|
|
(settings.mapShowRepeaters ||
|
|
|
|
|
_isBuildingPathTrace ||
|
|
|
|
|
settings.mapShowOverlaps)) {
|
|
|
|
|
addContact = true;
|
|
|
|
|
}
|
|
|
|
|
if (contact.type == advTypeChat &&
|
|
|
|
|
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
|
|
|
|
|
continue;
|
|
|
|
|
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
|
|
|
|
|
addContact = true;
|
|
|
|
|
}
|
|
|
|
|
if (contact.type != advTypeChat &&
|
|
|
|
|
contact.type != advTypeRepeater &&
|
|
|
|
|
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
|
|
|
|
|
continue;
|
|
|
|
|
(settings.mapShowOtherNodes ||
|
|
|
|
|
_isBuildingPathTrace ||
|
|
|
|
|
settings.mapShowOverlaps)) {
|
|
|
|
|
addContact = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final hasOverlap = contacts
|
|
|
|
|
.where(
|
|
|
|
|
(c) =>
|
|
|
|
|
c.publicKeyHex != contact.publicKeyHex &&
|
|
|
|
|
c.publicKey.first == contact.publicKey.first &&
|
|
|
|
|
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
|
|
|
|
(contact.type == advTypeRepeater ||
|
|
|
|
|
contact.type == advTypeRoom),
|
|
|
|
|
)
|
|
|
|
|
.firstOrNull;
|
|
|
|
|
|
|
|
|
|
if (hasOverlap == null &&
|
|
|
|
|
settings.mapShowOverlaps &&
|
|
|
|
|
!_isBuildingPathTrace) {
|
|
|
|
|
addContact = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (addContact) {
|
|
|
|
|
filtered.add(contact);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return filtered;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Marker> _buildMarkers(
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
settings, {
|
|
|
|
|
required bool showLabels,
|
|
|
|
|
}) {
|
|
|
|
|
final markers = <Marker>[];
|
|
|
|
|
final filteredContacts = _filterContactsBySettings(contacts, settings);
|
|
|
|
|
for (final contact in filteredContacts) {
|
|
|
|
|
final marker = Marker(
|
|
|
|
|
point: LatLng(contact.latitude!, contact.longitude!),
|
|
|
|
|
width: 35,
|
|
|
|
|
@ -852,7 +906,9 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(4),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: _getNodeColor(contact.type),
|
|
|
|
|
color: settings.mapShowOverlaps && !_isBuildingPathTrace
|
|
|
|
|
? Colors.red
|
|
|
|
|
: _getNodeColor(contact.type),
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
@ -879,7 +935,9 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
markers.add(
|
|
|
|
|
_buildNodeLabelMarker(
|
|
|
|
|
point: LatLng(contact.latitude!, contact.longitude!),
|
|
|
|
|
label: contact.name,
|
|
|
|
|
label: settings.mapShowOverlaps && !_isBuildingPathTrace
|
|
|
|
|
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
|
|
|
|
|
: contact.name,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@ -959,20 +1017,12 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
int markerCount,
|
|
|
|
|
int guessedCount,
|
|
|
|
|
) {
|
|
|
|
|
int nodeCount = 0;
|
|
|
|
|
for (final contact in contactsWithLocation) {
|
|
|
|
|
// Apply node type filters
|
|
|
|
|
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
|
|
|
|
if (contact.type != advTypeChat &&
|
|
|
|
|
contact.type != advTypeRepeater &&
|
|
|
|
|
!settings.mapShowOtherNodes) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
nodeCount++;
|
|
|
|
|
}
|
|
|
|
|
final filteredContacts = _filterContactsBySettings(
|
|
|
|
|
contactsWithLocation,
|
|
|
|
|
settings,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final nodeCount = filteredContacts.length;
|
|
|
|
|
|
|
|
|
|
return Positioned(
|
|
|
|
|
top: 16,
|
|
|
|
|
@ -1846,6 +1896,15 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
},
|
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
|
),
|
|
|
|
|
CheckboxListTile(
|
|
|
|
|
title: Text(context.l10n.map_showOverlaps),
|
|
|
|
|
value: settings.mapShowOverlaps,
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
service.setMapShowOverlaps(value ?? true);
|
|
|
|
|
},
|
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(
|
|
|
|
|
context.l10n.map_keyPrefix,
|
|
|
|
|
@ -2004,12 +2063,13 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _startPath() {
|
|
|
|
|
void _startPath(LatLng position) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isBuildingPathTrace = true;
|
|
|
|
|
_pathTrace.clear();
|
|
|
|
|
_points.clear();
|
|
|
|
|
_polylines.clear();
|
|
|
|
|
_points.add(position);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -2055,14 +2115,33 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
.join(','),
|
|
|
|
|
style: TextStyle(fontSize: 18),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
// const SizedBox(height: 6),
|
|
|
|
|
Wrap(
|
|
|
|
|
alignment: WrapAlignment.center,
|
|
|
|
|
spacing: 8,
|
|
|
|
|
runSpacing: 8,
|
|
|
|
|
spacing: 1,
|
|
|
|
|
runSpacing: 1,
|
|
|
|
|
children: [
|
|
|
|
|
if (_pathTrace.isNotEmpty)
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (context) => PathTraceMapScreen(
|
|
|
|
|
title: l10n.contacts_pathTrace,
|
|
|
|
|
path: Uint8List.fromList(_pathTrace),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
setState(() {
|
|
|
|
|
_isBuildingPathTrace = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
tooltip: "Path Trace",
|
|
|
|
|
icon: const Icon(Icons.arrow_forward_outlined),
|
|
|
|
|
),
|
|
|
|
|
if (_pathTrace.isNotEmpty)
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
@ -2070,6 +2149,7 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
builder: (context) => PathTraceMapScreen(
|
|
|
|
|
title: l10n.contacts_pathTrace,
|
|
|
|
|
path: Uint8List.fromList(_pathTrace),
|
|
|
|
|
flipPathAround: true,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
@ -2077,15 +2157,17 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
_isBuildingPathTrace = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
child: Text(l10n.map_runTrace),
|
|
|
|
|
tooltip: "Build Return Path",
|
|
|
|
|
icon: const Icon(Icons.replay),
|
|
|
|
|
),
|
|
|
|
|
if (_pathTrace.isNotEmpty)
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: _removePath,
|
|
|
|
|
child: Text(l10n.map_removeLast),
|
|
|
|
|
tooltip: "Remove Last Point",
|
|
|
|
|
icon: const Icon(Icons.delete),
|
|
|
|
|
),
|
|
|
|
|
if (_pathTrace.isEmpty)
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isBuildingPathTrace = false;
|
|
|
|
|
@ -2097,7 +2179,7 @@ class _MapScreenState extends State<MapScreen> {
|
|
|
|
|
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
child: Text(l10n.common_cancel),
|
|
|
|
|
icon: const Icon(Icons.close),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|