Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478)

Co-authored-by: Garth Vander Houwen <garth@meshtastic.com>
This commit is contained in:
Mike Robbins 2025-10-21 09:23:22 -04:00 committed by GitHub
parent 1d49e022be
commit 6c3c02237b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 0 deletions

View file

@ -96,10 +96,40 @@ extension PositionEntity {
}
return pointAnn
}
var isPreciseLocation: Bool {
precisionBits == 32 || precisionBits == 0
}
var fuzzedNodeCoordinate: CLLocationCoordinate2D? {
// With reduced precisionBits, many nodes can overlap on the map, making them unclickable.
// Use a hash of the position ID to fuzz coordinate slightly so that these nodes can be distinguished at the higest zoom levels. This allows them to be clicked individually.
if latitudeI != 0 && longitudeI != 0 {
// Derive two uniform pseudorandom numbers [0,1) from id.hashValue
let u1 = Double(id.hashValue & 0xFFFF) / 65536.0
let u2 = Double((id.hashValue >> 16) & 0xFFFF) / 65536.0
// Angle and radius
let offsetAngle = 2.0 * .pi * u1
let offsetRadius = 0.00001 * sqrt(u2) // 1.0e-5 degrees at equator is about 1.11 m or 4 ft
let dLat = sin(offsetAngle) * offsetRadius
let dLon = cos(offsetAngle) * offsetRadius
let coord = CLLocationCoordinate2D(
latitude: latitude! + dLat,
longitude: longitude! + dLon
)
return coord
} else {
return nil
}
}
}
extension PositionEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationsHandler.DefaultLocation }
public var fuzzedCoordinate: CLLocationCoordinate2D { fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation }
public var title: String? { nodePosition?.user?.shortName ?? "Unknown".localized }
public var subtitle: String? { time?.formatted() }
}

View file

@ -49,6 +49,29 @@ struct MeshMapContent: MapContent {
ForEach(positions, id: \.id) { position in
/// Apply favorits filter and don't show ignored nodes
if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) {
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
let positionName = position.nodePosition?.user?.longName ?? "?"
// Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction.
let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5
let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation {
// Precise location: place node pin at actual location.
position.coordinate
} else {
// Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels.
position.fuzzedCoordinate
}
Annotation(positionName, coordinate: coordinateForNodePin) {
LazyVStack {
AnimatedNodePin(
nodeColor: nodeColor,
shortName: position.nodePosition?.user?.shortName,
hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false,
isOnline: position.nodePosition?.isOnline ?? false,
calculatedDelay: calculatedDelay
)
if 12...15 ~= position.precisionBits || position.precisionBits == 32 {
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))