66//
77
88import SwiftUI
9+ import Foundation
910
1011private struct TabDescriptor : Identifiable {
1112 let id : String
@@ -60,6 +61,147 @@ struct MainTabView: View {
6061 }
6162 selection = ids. first ?? settingsTab. id
6263 }
64+
65+ private func handleURL( _ url: URL ) {
66+ guard let host = url. host ( ) ? . lowercased ( ) else { return }
67+
68+ switch host {
69+ case " simulate-location " , " set-location " :
70+ simulateLocation ( from: url)
71+ case " location " , " location-simulation " :
72+ if coordinate ( from: url) == nil {
73+ openTab ( id: " location " )
74+ } else {
75+ simulateLocation ( from: url)
76+ }
77+ case " clear-location " , " stop-location " :
78+ clearSimulatedLocation ( )
79+ default :
80+ break
81+ }
82+ }
83+
84+ private func openTab( id: String ) {
85+ if displayTabs. contains ( where: { $0. id == id } ) {
86+ selection = id
87+ } else if let descriptor = availableTabs. first ( where: { $0. id == id } ) {
88+ detachedTab = descriptor
89+ }
90+ }
91+
92+ private func simulateLocation( from url: URL ) {
93+ guard let coordinate = coordinate ( from: url) else {
94+ showAlert (
95+ title: " Invalid Location URL " ,
96+ message: " Use stikdebug://simulate-location?lat=37.3349&lon=-122.0090 " ,
97+ showOk: true
98+ )
99+ return
100+ }
101+
102+ guard coordinateIsValid ( latitude: coordinate. latitude, longitude: coordinate. longitude) else {
103+ showAlert (
104+ title: " Invalid Coordinates " ,
105+ message: " Latitude must be between -90 and 90. Longitude must be between -180 and 180. " ,
106+ showOk: true
107+ )
108+ return
109+ }
110+
111+ let pairingFile = PairingFileStore . prepareURL ( )
112+ guard FileManager . default. fileExists ( atPath: pairingFile. path) else {
113+ showAlert (
114+ title: " Pairing File Required " ,
115+ message: " Import a pairing file before simulating location from a URL. " ,
116+ showOk: true
117+ )
118+ return
119+ }
120+
121+ LocationSimulationCommandQueue . shared. async {
122+ let code = simulate_location (
123+ DeviceConnectionContext . targetIPAddress,
124+ coordinate. latitude,
125+ coordinate. longitude,
126+ pairingFile. path
127+ )
128+
129+ DispatchQueue . main. async {
130+ if code == 0 {
131+ BackgroundLocationManager . shared. requestStart ( )
132+ LogManager . shared. addInfoLog (
133+ String ( format: " Simulated location from URL: %.6f, %.6f " , coordinate. latitude, coordinate. longitude)
134+ )
135+ } else {
136+ showAlert (
137+ title: " Location Simulation Failed " ,
138+ message: " Could not simulate location from URL (error \( code) ). Make sure the device is connected and the DDI is mounted. " ,
139+ showOk: true
140+ )
141+ }
142+ }
143+ }
144+ }
145+
146+ private func clearSimulatedLocation( ) {
147+ LocationSimulationCommandQueue . shared. async {
148+ let code = clear_simulated_location ( )
149+ DispatchQueue . main. async {
150+ if code == 0 {
151+ BackgroundLocationManager . shared. requestStop ( )
152+ LogManager . shared. addInfoLog ( " Cleared simulated location from URL " )
153+ } else {
154+ showAlert (
155+ title: " Clear Location Failed " ,
156+ message: " Could not clear simulated location from URL (error \( code) ). " ,
157+ showOk: true
158+ )
159+ }
160+ }
161+ }
162+ }
163+
164+ private func coordinate( from url: URL ) -> ( latitude: Double , longitude: Double ) ? {
165+ let components = URLComponents ( url: url, resolvingAgainstBaseURL: false )
166+ let queryItems = components? . queryItems ?? [ ]
167+
168+ func queryValue( _ names: [ String ] ) -> String ? {
169+ for name in names {
170+ if let value = queryItems. first ( where: { $0. name. caseInsensitiveCompare ( name) == . orderedSame } ) ? . value {
171+ return value
172+ }
173+ }
174+ return nil
175+ }
176+
177+ if let latitudeText = queryValue ( [ " lat " , " latitude " ] ) ,
178+ let longitudeText = queryValue ( [ " lon " , " lng " , " long " , " longitude " ] ) ,
179+ let latitude = Double ( latitudeText. trimmingCharacters ( in: . whitespacesAndNewlines) ) ,
180+ let longitude = Double ( longitudeText. trimmingCharacters ( in: . whitespacesAndNewlines) ) {
181+ return ( latitude, longitude)
182+ }
183+
184+ let coordinateText = queryValue ( [ " coordinate " , " coordinates " , " coords " , " q " , " ll " ] )
185+ ?? components? . path
186+ ?? " "
187+ let values = numbers ( in: coordinateText)
188+ guard values. count >= 2 else { return nil }
189+ return ( values [ 0 ] , values [ 1 ] )
190+ }
191+
192+ private func coordinateIsValid( latitude: Double , longitude: Double ) -> Bool {
193+ ( - 90.0 ... 90.0 ) . contains ( latitude) && ( - 180.0 ... 180.0 ) . contains ( longitude)
194+ }
195+
196+ private func numbers( in text: String ) -> [ Double ] {
197+ let pattern = #"[-+]?(?:\d+(?:\.\d*)?|\.\d+)"#
198+ guard let regex = try ? NSRegularExpression ( pattern: pattern) else { return [ ] }
199+ let range = NSRange ( text. startIndex..< text. endIndex, in: text)
200+ return regex. matches ( in: text, range: range) . compactMap { match in
201+ guard let matchRange = Range ( match. range, in: text) else { return nil }
202+ return Double ( text [ matchRange] )
203+ }
204+ }
63205
64206 private var displayTabs : [ TabDescriptor ] {
65207 var tabs = [ " home " , " tools " ] . compactMap { id in
@@ -95,11 +237,7 @@ struct MainTabView: View {
95237 }
96238 switchObserver = NotificationCenter . default. addObserver ( forName: . switchToTab, object: nil , queue: . main) { note in
97239 guard let id = note. object as? String else { return }
98- if selectedTabDescriptors. contains ( where: { $0. id == id } ) {
99- selection = id
100- } else if let descriptor = availableTabs. first ( where: { $0. id == id } ) {
101- detachedTab = descriptor
102- }
240+ openTab ( id: id)
103241 }
104242 }
105243 . onDisappear {
@@ -111,6 +249,9 @@ struct MainTabView: View {
111249 . onChange ( of: enabledTabIdentifiers) { _, _ in
112250 ensureSelectionIsValid ( )
113251 }
252+ . onOpenURL { url in
253+ handleURL ( url)
254+ }
114255 . sheet ( item: $detachedTab) { descriptor in
115256 NavigationStack {
116257 descriptor. builder ( )
0 commit comments