diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 3efa9af3..080693c2 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -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() } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 9ccf3f76..4edb6ac4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -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))