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
}
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-09-18 13:19:45 -07: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-09-18 13:19:45 -07:00
if ( ! showFavorites || ( position . nodePosition ? . favorite = = true ) ) && ! ( position . nodePosition ? . ignored = = true ) {
2025-05-23 20:53:31 -07:00
let nodeColor = UIColor ( hex : UInt32 ( position . nodePosition ? . num ? ? 0 ) )
let positionName = position . nodePosition ? . user ? . longName ? ? " ? "
2025-09-18 13:19:45 -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-05-23 20:53:31 -07:00
Annotation ( positionName , coordinate : position . coordinate ) {
2025-09-18 13:19:45 -07:00
LazyVStack {
AnimatedNodePin (
nodeColor : nodeColor ,
shortName : position . nodePosition ? . user ? . shortName ,
hasDetectionSensorMetrics : position . nodePosition ? . hasDetectionSensorMetrics ? ? false ,
isOnline : position . nodePosition ? . isOnline ? ? false ,
calculatedDelay : calculatedDelay
)
2024-03-23 09:01:44 -07:00
}
2025-09-18 13:19:45 -07:00
. highPriorityGesture ( TapGesture ( ) . onEnded { _ in
selectedPosition = ( selectedPosition = = position ? nil : position )
2024-03-24 22:56:38 -07:00
} )
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-09-18 13:19:45 -07:00
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
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
}
}