Skip to content

Commit f4fdf44

Browse files
jfuginayclaude
andcommitted
Fix build after merge and restore singleton pattern
- Restore MeshtasticManager.shared singleton pattern - Add COT bridge configuration to MeshtasticManager init - Add MeshtasticCOTBridge.swift to Xcode project - Fix MeshtasticConnectionView to pass manager to DevicePicker - Apply ADSB aircraft annotation improvements from stash 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 467405b commit f4fdf44

File tree

6 files changed

+170
-60
lines changed

6 files changed

+170
-60
lines changed

apps/omnitak/OmniTAKMobile.xcodeproj/project.pbxproj

Lines changed: 34 additions & 30 deletions
Large diffs are not rendered by default.

apps/omnitak/OmniTAKMobile/Features/ADSB/Views/AircraftAnnotation.swift

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import SwiftUI
1111
// MARK: - Aircraft Annotation
1212

1313
class AircraftAnnotation: NSObject, MKAnnotation {
14-
let aircraft: Aircraft
14+
private(set) var aircraft: Aircraft
1515

16-
var coordinate: CLLocationCoordinate2D {
17-
aircraft.coordinate
18-
}
16+
// Dynamic coordinate for smooth updates
17+
@objc dynamic var coordinate: CLLocationCoordinate2D
18+
19+
// Track if annotation needs visual update
20+
var needsVisualUpdate: Bool = false
1921

2022
var title: String? {
2123
aircraft.callsign.isEmpty ? aircraft.id.uppercased() : aircraft.callsign
@@ -47,17 +49,36 @@ class AircraftAnnotation: NSObject, MKAnnotation {
4749

4850
init(aircraft: Aircraft) {
4951
self.aircraft = aircraft
52+
self.coordinate = aircraft.coordinate
5053
super.init()
5154
}
55+
56+
/// Update aircraft data in-place without removing annotation
57+
func update(with newAircraft: Aircraft) {
58+
let positionChanged = coordinate.latitude != newAircraft.coordinate.latitude ||
59+
coordinate.longitude != newAircraft.coordinate.longitude
60+
let headingChanged = abs(aircraft.heading - newAircraft.heading) > 1.0
61+
let altitudeChanged = abs(aircraft.altitude - newAircraft.altitude) > 10
62+
63+
aircraft = newAircraft
64+
65+
// Update coordinate (triggers MKMapView position update via KVO)
66+
if positionChanged {
67+
coordinate = newAircraft.coordinate
68+
}
69+
70+
// Flag for visual update if heading or altitude changed significantly
71+
needsVisualUpdate = headingChanged || altitudeChanged
72+
}
5273
}
5374

5475
// MARK: - Aircraft Annotation View
5576

5677
class AircraftAnnotationView: MKAnnotationView {
5778
static let reuseIdentifier = "AircraftAnnotationView"
5879

59-
private var imageView: UIImageView?
6080
private var hostingController: UIHostingController<AircraftMapIcon>?
81+
private var currentAircraftId: String?
6182

6283
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
6384
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
@@ -82,6 +103,20 @@ class AircraftAnnotationView: MKAnnotationView {
82103
}
83104
}
84105

106+
/// Call this to refresh the view when aircraft data updates in-place
107+
func refreshIfNeeded() {
108+
guard let aircraftAnnotation = annotation as? AircraftAnnotation,
109+
aircraftAnnotation.needsVisualUpdate else { return }
110+
111+
// Update the hosting controller's root view instead of recreating
112+
if let hostingController = hostingController {
113+
hostingController.rootView = AircraftMapIcon(aircraft: aircraftAnnotation.aircraft)
114+
}
115+
116+
updateDisplayPriority(for: aircraftAnnotation.aircraft)
117+
aircraftAnnotation.needsVisualUpdate = false
118+
}
119+
85120
private func updateView(for aircraftAnnotation: AircraftAnnotation) {
86121
let aircraft = aircraftAnnotation.aircraft
87122
let category = aircraftAnnotation.category
@@ -91,23 +126,33 @@ class AircraftAnnotationView: MKAnnotationView {
91126
let baseSize: CGFloat = category.iconSize * sizeClass.scaleFactor
92127
let viewSize = baseSize + 8 // Add padding for background
93128

94-
// Remove existing hosting controller
95-
hostingController?.view.removeFromSuperview()
129+
// Only recreate hosting controller if it doesn't exist or aircraft ID changed
130+
if hostingController == nil || currentAircraftId != aircraft.id {
131+
hostingController?.view.removeFromSuperview()
96132

97-
// Create SwiftUI icon view
98-
let iconView = AircraftMapIcon(aircraft: aircraft)
99-
let hostingVC = UIHostingController(rootView: iconView)
100-
hostingVC.view.backgroundColor = UIColor.clear
101-
hostingVC.view.frame = CGRect(x: -viewSize/2, y: -viewSize/2, width: viewSize, height: viewSize)
133+
let iconView = AircraftMapIcon(aircraft: aircraft)
134+
let hostingVC = UIHostingController(rootView: iconView)
135+
hostingVC.view.backgroundColor = UIColor.clear
136+
hostingVC.view.frame = CGRect(x: -viewSize/2, y: -viewSize/2, width: viewSize, height: viewSize)
102137

103-
addSubview(hostingVC.view)
104-
hostingController = hostingVC
138+
addSubview(hostingVC.view)
139+
hostingController = hostingVC
140+
currentAircraftId = aircraft.id
141+
} else {
142+
// Just update the root view without recreating the controller
143+
hostingController?.rootView = AircraftMapIcon(aircraft: aircraft)
144+
hostingController?.view.frame = CGRect(x: -viewSize/2, y: -viewSize/2, width: viewSize, height: viewSize)
145+
}
105146

106147
// Set frame and center offset
107148
frame = CGRect(x: 0, y: 0, width: viewSize, height: viewSize)
108149
centerOffset = CGPoint(x: 0, y: 0)
109150

110-
// Set display priority based on altitude (higher planes more visible)
151+
updateDisplayPriority(for: aircraft)
152+
aircraftAnnotation.needsVisualUpdate = false
153+
}
154+
155+
private func updateDisplayPriority(for aircraft: Aircraft) {
111156
let altFeet = aircraft.altitude * 3.28084
112157
if altFeet > 35000 {
113158
displayPriority = .defaultHigh
@@ -117,4 +162,10 @@ class AircraftAnnotationView: MKAnnotationView {
117162
displayPriority = .defaultLow
118163
}
119164
}
165+
166+
override func prepareForReuse() {
167+
super.prepareForReuse()
168+
// Don't remove the hosting controller - just reset the tracking ID
169+
currentAircraftId = nil
170+
}
120171
}

apps/omnitak/OmniTAKMobile/Features/Map/Controllers/MapViewController.swift

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,12 +1687,14 @@ struct TacticalMapView: UIViewRepresentable {
16871687
}
16881688

16891689
private func updateAnnotations(mapView: MKMapView, markers: [CoTMarker], aircraft: [Aircraft], context: Context) {
1690-
// Remove old CoT and aircraft annotations (but keep drawing annotations)
1691-
let oldAnnotations = mapView.annotations.filter { annotation in
1690+
// Remove old CoT annotations only (not aircraft - they're updated incrementally)
1691+
let oldCotAnnotations = mapView.annotations.filter { annotation in
1692+
annotation is MKPointAnnotation &&
16921693
!(annotation is MKUserLocation) &&
1693-
!context.coordinator.isDrawingAnnotation(annotation)
1694+
!context.coordinator.isDrawingAnnotation(annotation) &&
1695+
!(annotation is AircraftAnnotation)
16941696
}
1695-
mapView.removeAnnotations(oldAnnotations)
1697+
mapView.removeAnnotations(oldCotAnnotations)
16961698

16971699
// Add new CoT annotations
16981700
let cotAnnotations = markers.map { marker -> MKPointAnnotation in
@@ -1704,14 +1706,54 @@ struct TacticalMapView: UIViewRepresentable {
17041706
}
17051707
mapView.addAnnotations(cotAnnotations)
17061708

1707-
// Add aircraft annotations
1708-
let aircraftAnnotations = aircraft.map { AircraftAnnotation(aircraft: $0) }
1709-
mapView.addAnnotations(aircraftAnnotations)
1709+
// Update aircraft annotations incrementally (no flicker)
1710+
updateAircraftAnnotations(mapView: mapView, aircraft: aircraft)
17101711

17111712
// Update drawing annotations
17121713
updateDrawingAnnotations(mapView: mapView, context: context)
17131714
}
17141715

1716+
private func updateAircraftAnnotations(mapView: MKMapView, aircraft: [Aircraft]) {
1717+
// Get existing aircraft annotations
1718+
let existingAircraftAnnotations = mapView.annotations.compactMap { $0 as? AircraftAnnotation }
1719+
var existingById: [String: AircraftAnnotation] = [:]
1720+
for annotation in existingAircraftAnnotations {
1721+
existingById[annotation.aircraft.id] = annotation
1722+
}
1723+
1724+
// Create set of current aircraft IDs
1725+
let currentIds = Set(aircraft.map { $0.id })
1726+
1727+
// Remove departed aircraft
1728+
let departedAnnotations = existingAircraftAnnotations.filter { !currentIds.contains($0.aircraft.id) }
1729+
if !departedAnnotations.isEmpty {
1730+
mapView.removeAnnotations(departedAnnotations)
1731+
}
1732+
1733+
// Update existing and add new aircraft
1734+
var newAnnotations: [AircraftAnnotation] = []
1735+
for aircraftData in aircraft {
1736+
if let existingAnnotation = existingById[aircraftData.id] {
1737+
// Update existing annotation in-place (no remove/add)
1738+
existingAnnotation.update(with: aircraftData)
1739+
1740+
// Refresh view if needed
1741+
if existingAnnotation.needsVisualUpdate,
1742+
let view = mapView.view(for: existingAnnotation) as? AircraftAnnotationView {
1743+
view.refreshIfNeeded()
1744+
}
1745+
} else {
1746+
// New aircraft - add annotation
1747+
newAnnotations.append(AircraftAnnotation(aircraft: aircraftData))
1748+
}
1749+
}
1750+
1751+
// Batch add new annotations
1752+
if !newAnnotations.isEmpty {
1753+
mapView.addAnnotations(newAnnotations)
1754+
}
1755+
}
1756+
17151757
private func updateDrawingAnnotations(mapView: MKMapView, context: Context) {
17161758
// Remove old drawing annotations
17171759
let oldDrawingAnnotations = mapView.annotations.filter { context.coordinator.isDrawingAnnotation($0) }

apps/omnitak/OmniTAKMobile/Features/Meshtastic/Managers/MeshtasticManager.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import CoreBluetooth
1313
@MainActor
1414
public class MeshtasticManager: ObservableObject {
1515

16+
// MARK: - Singleton
17+
18+
/// Shared instance for app-wide Meshtastic management
19+
public static let shared = MeshtasticManager()
20+
1621
// MARK: - Published Properties
1722

1823
@Published public var connectedDevice: MeshtasticDevice?
@@ -59,7 +64,15 @@ public class MeshtasticManager: ObservableObject {
5964
// MARK: - Initialization
6065

6166
public init() {
62-
// TCP client is lazily initialized when needed
67+
// TCP and BLE clients are lazily initialized when needed
68+
// Configure COT bridge to convert mesh nodes to map markers
69+
configureCOTBridge()
70+
}
71+
72+
/// Configure the COT bridge for converting Meshtastic data to TAK format
73+
private func configureCOTBridge() {
74+
MeshtasticCOTBridge.shared.configure(meshtasticManager: self)
75+
print("MeshtasticManager: COT bridge configured")
6376
}
6477

6578
// MARK: - TCP Client Setup

apps/omnitak/OmniTAKMobile/Features/Meshtastic/Views/MeshtasticConnectionView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ struct MeshtasticConnectionView: View {
7777
}
7878
}
7979
.sheet(isPresented: $showingDevicePicker) {
80-
MeshtasticDevicePickerView()
80+
MeshtasticDevicePickerView(manager: manager)
8181
}
8282
.sheet(isPresented: $showingMeshTopology) {
8383
MeshTopologyView()

apps/omnitak/OmniTAKMobile/Resources/Info.plist

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>BGTaskSchedulerPermittedIdentifiers</key>
6+
<array>
7+
<string>com.engindearing.omnitak.mobile.refresh</string>
8+
<string>com.engindearing.omnitak.mobile.processing</string>
9+
</array>
510
<key>CFBundleIconName</key>
611
<string>AppIcon</string>
712
<key>CFBundleURLTypes</key>
@@ -47,13 +52,8 @@
4752
<string>location</string>
4853
<string>bluetooth-central</string>
4954
<string>fetch</string>
55+
<string>remote-notification</string>
5056
<string>processing</string>
51-
<string>external-accessory</string>
52-
</array>
53-
<key>BGTaskSchedulerPermittedIdentifiers</key>
54-
<array>
55-
<string>com.engindearing.omnitak.mobile.refresh</string>
56-
<string>com.engindearing.omnitak.mobile.processing</string>
5757
</array>
5858
<key>UILaunchScreen</key>
5959
<dict/>

0 commit comments

Comments
 (0)