2024-03-23 09:01:44 -07:00
//
// M e s h M a p C o n t e n t . s w i f t
// M e s h t a s t i c
//
// C r e a t e d b y G a r t h V a n d e r H o u w e n o n 3 / 1 7 / 2 4 .
//
import SwiftUI
import MapKit
2025-07-22 00:48:50 +00:00
import CoreLocation
import OSLog
2024-03-23 09:01:44 -07:00
2025-07-16 02:45:17 +00:00
struct IdentifiableOverlay : Identifiable {
2025-09-18 13:19:45 -07:00
let overlay : MKOverlay
var id : ObjectIdentifier { ObjectIdentifier ( overlay as AnyObject ) }
2025-07-16 02:45:17 +00:00
}
2025-10-30 17:15:18 -04:00
struct ReducedPrecisionMapCircleKey : Hashable {
let latitudeI : Int32
let longitudeI : Int32
let precisionBits : Int32
}
2024-03-23 09:01:44 -07:00
struct MeshMapContent : MapContent {
2025-09-18 13:19:45 -07:00
2024-03-23 09:01:44 -07:00
// / P a r a m e t e r s
@ Binding var showUserLocation : Bool
2024-03-25 18:43:03 -07:00
@ AppStorage ( " meshMapShowNodeHistory " ) private var showNodeHistory = false
@ AppStorage ( " meshMapShowRouteLines " ) private var showRouteLines = false
@ AppStorage ( " enableMapConvexHull " ) private var showConvexHull = false
2025-05-24 00:19:00 -07:00
@ AppStorage ( " enableMapShowFavorites " ) private var showFavorites = false
2024-03-23 09:01:44 -07:00
@ Binding var showTraffic : Bool
@ Binding var showPointsOfInterest : Bool
@ Binding var selectedMapLayer : MapLayer
// M a p C o n f i g u r a t i o n
@ Binding var selectedPosition : PositionEntity ?
2025-05-05 17:21:08 -07:00
@ AppStorage ( " enableMapWaypoints " ) private var showWaypoints = true
2024-03-23 09:01:44 -07:00
@ Binding var selectedWaypoint : WaypointEntity ?
2025-07-21 21:42:36 +00:00
// M a p o v e r l a y s
@ AppStorage ( " mapOverlaysEnabled " ) private var showMapOverlays = false
2025-07-22 02:03:36 +00:00
@ Binding var enabledOverlayConfigs : Set < UUID >
2025-09-18 13:19:45 -07:00
2024-03-25 15:21:38 -07:00
@ FetchRequest ( fetchRequest : PositionEntity . allPositionsFetchRequest ( ) , animation : . easeIn )
var positions : FetchedResults < PositionEntity >
2025-09-18 13:19:45 -07:00
2024-03-25 15:21:38 -07:00
@ FetchRequest ( fetchRequest : WaypointEntity . allWaypointssFetchRequest ( ) , animation : . none )
var waypoints : FetchedResults < WaypointEntity >
2025-09-18 13:19:45 -07:00
2024-03-25 19:20:36 -07:00
@ FetchRequest ( sortDescriptors : [ NSSortDescriptor ( key : " name " , ascending : true ) ] ,
predicate : NSPredicate ( format : " enabled == true " , " " ) , animation : . none )
private var routes : FetchedResults < RouteEntity >
2025-10-30 17:15:18 -04:00
2024-03-23 09:01:44 -07:00
@ MapContentBuilder
2024-05-29 16:40:07 -05:00
var positionAnnotations : some MapContent {
ForEach ( positions , id : \ . id ) { position in
2025-10-26 18:40:37 -07:00
// / A p p l y f a v o r i t e s f i l t e r a n d d o n ' t s h o w i g n o r e d n o d e s
2025-09-18 13:19:45 -07:00
if ( ! showFavorites || ( position . nodePosition ? . favorite = = true ) ) && ! ( position . nodePosition ? . ignored = = true ) {
2025-10-21 09:23:22 -04:00
let coordinateForNodePin : CLLocationCoordinate2D = if position . isPreciseLocation {
// P r e c i s e l o c a t i o n : p l a c e n o d e p i n a t a c t u a l l o c a t i o n .
position . coordinate
} else {
// I m p r e c i s e l o c a t i o n : f u z z s l i g h t l y s o o v e r l a p p i n g n o d e s a r e v i s i b l e a n d c l i c k a b l e a t h i g h e s t z o o m l e v e l s .
position . fuzzedCoordinate
}
2025-10-20 11:33:58 -07:00
if 12. . . 15 ~= position . precisionBits || position . precisionBits = = 32 {
let nodeColor = UIColor ( hex : UInt32 ( position . nodePosition ? . num ? ? 0 ) )
let positionName = position . nodePosition ? . user ? . longName ? ? " ? "
2025-10-30 17:15:18 -04:00
2025-10-20 11:33:58 -07:00
// U s e a h a s h o f t h e p o s i t i o n I D t o s t a g g e r a n i m a t i o n d e l a y s f o r e a c h n o d e , p r e v e n t i n g s y n c h r o n i z e d a n i m a t i o n s a n d i m p r o v i n g v i s u a l d i s t i n c t i o n .
let calculatedDelay = Double ( position . id . hashValue % 100 ) / 100.0 * 0.5
2025-10-21 06:28:33 -07:00
Annotation ( positionName , coordinate : coordinateForNodePin ) {
2025-10-20 11:33:58 -07:00
LazyVStack {
AnimatedNodePin (
nodeColor : nodeColor ,
shortName : position . nodePosition ? . user ? . shortName ,
hasDetectionSensorMetrics : position . nodePosition ? . hasDetectionSensorMetrics ? ? false ,
isOnline : position . nodePosition ? . isOnline ? ? false ,
calculatedDelay : calculatedDelay
)
}
. highPriorityGesture ( TapGesture ( ) . onEnded { _ in
selectedPosition = ( selectedPosition = = position ? nil : position )
} )
2024-03-23 09:01:44 -07:00
}
}
}
2024-05-29 16:40:07 -05:00
}
2025-05-23 20:53:31 -07:00
}
2025-10-30 17:15:18 -04:00
private var reducedPrecisionCircleItems : [ ( nodeNum : Int64 , circleKey : ReducedPrecisionMapCircleKey ) ] {
// P r e c o m p u t e * u n i q u e * r e d u c e d - p r e c i s i o n c i r c l e s s o w e d o n ' t h a v e t o r e d r a w t o n s o f i d e n t i c a l ( c e n t e r , r a d i u s ) c i r c l e s i n d e n s e m a p a r e a s . ( S i n c e t h e y ' r e a l l t r a n s p a r e n t , t h i s c a u s e s s e v e r e F P S d r o p w h e n z o o m e d i n t o a r e a s w h e r e t h e r e a r e a t o n o f o v e r l a p p i n g c i r c l e s . )
var lowestNumForKey : [ ReducedPrecisionMapCircleKey : Int64 ] = [ : ]
// P o p u l a t e a d i c t w h e r e t h e k e y i s ( l a t , l o n , b i t s ) a n d t h e v a l u e i s t h e * l o w e s t * n o d e . n u m s e e n f o r t h a t k e y .
// T h a t l o w e s t n o d e . n u m v a l u e i s u s e d t o c r e a t e a s t a b l e c o l o r f o r t h e M a p C i r c l e a n d s t a b l e i d f o r F o r E a c h .
for position in positions {
// S a m e f i l t e r c r i t e r i a a s p o s i t i o n A n n o t a t i o n s :
if ( ! showFavorites || ( position . nodePosition ? . favorite = = true ) ) && ! ( position . nodePosition ? . ignored = = true ) {
if 12. . . 15 ~= position . precisionBits {
let nodeNum = position . nodePosition ? . num ? ? 0
let key = ReducedPrecisionMapCircleKey ( latitudeI : position . latitudeI , longitudeI : position . longitudeI , precisionBits : position . precisionBits )
if let existing = lowestNumForKey [ key ] {
if nodeNum < existing { lowestNumForKey [ key ] = nodeNum }
} else {
lowestNumForKey [ key ] = nodeNum
}
}
}
}
// S o r t b y n o d e N u m j u s t t o k e e p d r a w o r d e r s t a b l e .
return lowestNumForKey . map { ( $0 . value , $0 . key ) } . sorted { $0 . nodeNum < $1 . nodeNum }
}
@ MapContentBuilder
var reducedPrecisionMapCircles : some MapContent {
ForEach ( reducedPrecisionCircleItems , id : \ . nodeNum ) { item in
let circleKey = item . circleKey
let nodeNum = item . nodeNum
let radius = PositionPrecision ( rawValue : Int ( circleKey . precisionBits ) ) ? . precisionMeters ? ? 0
if radius > 0.0 {
let center = CLLocationCoordinate2D ( latitude : Double ( circleKey . latitudeI ) / 1e7 , longitude : Double ( circleKey . longitudeI ) / 1e7 )
let nodeColor = UIColor ( hex : UInt32 ( nodeNum ) )
MapCircle ( center : center , radius : radius )
. foregroundStyle ( Color ( nodeColor ) . opacity ( 0.25 ) )
. stroke ( . white , lineWidth : 1 )
}
}
}
2024-05-29 16:40:07 -05:00
@ MapContentBuilder
var routeAnnotations : some MapContent {
ForEach ( routes ) { route in
if let routeLocations = route . locations , let locations = Array ( routeLocations ) as ? [ LocationEntity ] {
let routeCoords = locations . compactMap { ( loc ) -> CLLocationCoordinate2D in
2025-01-21 09:19:14 -08:00
return loc . locationCoordinate ? ? LocationsHandler . DefaultLocation
2024-05-29 16:40:07 -05:00
}
2025-09-12 03:49:47 +02:00
Annotation ( String ( localized : " Start " ) , coordinate : routeCoords . first ? ? LocationsHandler . DefaultLocation ) {
2024-03-23 09:01:44 -07:00
ZStack {
Circle ( )
. fill ( Color ( . green ) )
. strokeBorder ( . white , lineWidth : 3 )
. frame ( width : 15 , height : 15 )
}
}
. annotationTitles ( . automatic )
2025-09-12 03:49:47 +02:00
Annotation ( String ( localized : " Finish " , comment : " Space at the end has been added to not interfere with translations for 'Finish' in RouteRecorder " ) , coordinate : routeCoords . last ? ? LocationsHandler . DefaultLocation ) {
2024-03-23 09:01:44 -07:00
ZStack {
Circle ( )
. fill ( Color ( . black ) )
. strokeBorder ( . white , lineWidth : 3 )
. frame ( width : 15 , height : 15 )
}
}
. annotationTitles ( . automatic )
let solid = StrokeStyle (
lineWidth : 3 ,
lineCap : . round , lineJoin : . round
)
MapPolyline ( coordinates : routeCoords )
. stroke ( Color ( UIColor ( hex : UInt32 ( route . color ) ) ) , style : solid )
}
}
2024-05-29 16:40:07 -05:00
}
2025-09-18 13:19:45 -07:00
2024-05-29 16:40:07 -05:00
@ MapContentBuilder
var waypointAnnotations : some MapContent {
if waypoints . count > 0 , showWaypoints , let waypoints = Array ( waypoints ) as ? [ WaypointEntity ] {
ForEach ( waypoints , id : \ . self ) { waypoint in
2024-03-23 09:01:44 -07:00
Annotation ( waypoint . name ? ? " ? " , coordinate : waypoint . coordinate ) {
LazyVStack {
2024-03-25 15:21:38 -07:00
ZStack {
CircleText ( text : String ( UnicodeScalar ( Int ( waypoint . icon ) ) ? ? " 📍 " ) , color : Color . orange , circleSize : 40 )
2025-05-05 22:06:08 -07:00
. highPriorityGesture ( TapGesture ( ) . onEnded { _ in
2024-03-25 15:21:38 -07:00
selectedWaypoint = ( selectedWaypoint = = waypoint ? nil : waypoint )
} )
}
2024-03-23 09:01:44 -07:00
}
}
}
}
}
2025-09-18 13:19:45 -07:00
2024-05-29 16:40:07 -05:00
@ MapContentBuilder
var meshMap : some MapContent {
let loraNodes = positions . filter { $0 . nodePosition ? . viaMqtt ? ? true = = false }
let loraCoords = Array ( loraNodes ) . compactMap ( { ( position ) -> CLLocationCoordinate2D in
return position . nodeCoordinate ? ? LocationsHandler . DefaultLocation
} )
// / C o n v e x H u l l
if showConvexHull {
if loraCoords . count > 0 {
let hull = loraCoords . getConvexHull ( )
MapPolygon ( coordinates : hull )
. stroke ( . blue , lineWidth : 3 )
. foregroundStyle ( . indigo . opacity ( 0.4 ) )
}
}
2025-09-18 13:19:45 -07:00
2025-07-22 00:48:50 +00:00
// / G e o J S O N O v e r l a y s w i t h e m b e d d e d s t y l i n g
2025-07-21 21:42:36 +00:00
if showMapOverlays {
2025-07-22 00:48:50 +00:00
overlayContent
}
2025-09-18 13:19:45 -07:00
2025-07-22 00:48:50 +00:00
positionAnnotations
2025-10-30 17:15:18 -04:00
reducedPrecisionMapCircles
2025-07-22 00:48:50 +00:00
routeAnnotations
waypointAnnotations
}
2025-09-18 13:19:45 -07:00
2025-07-22 00:48:50 +00:00
var overlayContent : some MapContent {
2025-07-22 02:03:36 +00:00
// G e t a l l f e a t u r e s b u t f i l t e r b y e n a b l e d c o n f i g s
let allStyledFeatures = GeoJSONOverlayManager . shared . loadStyledFeaturesForConfigs ( enabledOverlayConfigs )
2025-09-18 13:19:45 -07:00
2025-07-22 00:48:50 +00:00
return Group {
2025-07-22 02:03:36 +00:00
ForEach ( 0. . < allStyledFeatures . count , id : \ . self ) { index in
let styledFeature = allStyledFeatures [ index ]
2025-07-22 00:48:50 +00:00
let feature = styledFeature . feature
let geometryType = feature . geometry . type
2025-09-18 13:19:45 -07:00
2025-07-22 00:48:50 +00:00
if geometryType = = " Point " {
if let coordinate = feature . geometry . coordinates . toCoordinate ( ) {
2025-07-22 06:40:43 +00:00
Annotation ( feature . name , coordinate : coordinate ) {
2025-07-22 00:48:50 +00:00
Circle ( )
. fill ( styledFeature . fillColor )
. stroke ( styledFeature . strokeColor , style : styledFeature . strokeStyle )
. frame ( width : feature . markerRadius * 2 , height : feature . markerRadius * 2 )
}
2025-07-23 20:24:55 +00:00
. annotationTitles ( . automatic )
2025-07-22 00:48:50 +00:00
. annotationSubtitles ( . hidden )
}
} else if geometryType = = " LineString " {
if let overlay = styledFeature . createOverlay ( ) as ? MKPolyline {
MapPolyline ( overlay )
. stroke ( styledFeature . strokeColor , style : styledFeature . strokeStyle )
}
} else if geometryType = = " Polygon " {
if let overlay = styledFeature . createOverlay ( ) as ? MKPolygon {
MapPolygon ( overlay )
. foregroundStyle ( styledFeature . fillColor )
. stroke ( styledFeature . strokeColor , style : styledFeature . strokeStyle )
2025-07-18 01:28:21 +00:00
}
2025-07-16 02:45:17 +00:00
}
}
}
2024-05-29 16:40:07 -05:00
}
2025-09-18 13:19:45 -07:00
2024-03-23 09:01:44 -07:00
@ MapContentBuilder
var body : some MapContent {
2024-03-24 23:13:35 -07:00
meshMap
2024-03-23 09:01:44 -07:00
}
}