MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
ISSTracker.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "ISSTracker.h"
20
21#include "HttpClient.h"
22#include "Logger.h"
23
24#include <nlohmann/json.hpp>
25
26#include <chrono>
27#include <stop_token>
28#include <thread>
29
30namespace MetaImGUI {
31
32ISSTracker::ISSTracker() : m_tracking(false) {}
33
37
38void ISSTracker::StartTracking(std::function<void(const ISSPosition&)> callback) {
39 const std::lock_guard<std::mutex> lock(m_threadMutex);
40
41 if (m_tracking) {
42 LOG_INFO("ISS Tracker: Already tracking, skipping");
43 return;
44 }
45
46 m_tracking = true;
47
48 // Store callback under lock protection
49 {
50 const std::lock_guard<std::mutex> callbackLock(m_callbackMutex);
51 m_callback = callback;
52 }
53
54 // C++20: std::jthread with stop_token for clean cancellation
55 // jthread creates its own stop_source internally
56 m_trackingThread = std::jthread([this](const std::stop_token& stopToken) { TrackingLoop(stopToken); });
57
58 LOG_INFO("ISS Tracker: Started tracking");
59}
60
62 const std::lock_guard<std::mutex> lock(m_threadMutex);
63
64 if (!m_tracking) {
65 return;
66 }
67
68 // Request stop using jthread's internal stop_source
69 m_trackingThread.request_stop();
70 m_tracking = false;
71
72 LOG_INFO("ISS Tracker: Stopped tracking");
73}
74
76 return m_tracking;
77}
78
80 const std::lock_guard<std::mutex> lock(m_dataMutex);
81 return m_currentPosition;
82}
83
84void ISSTracker::GetPositionHistory(std::vector<double>& latitudes, std::vector<double>& longitudes) const {
85 const std::lock_guard<std::mutex> lock(m_dataMutex);
86
87 latitudes.clear();
88 longitudes.clear();
89 latitudes.reserve(m_positionHistory.size());
90 longitudes.reserve(m_positionHistory.size());
91
92 for (const auto& pos : m_positionHistory) {
93 if (pos.valid) {
94 latitudes.push_back(pos.latitude);
95 longitudes.push_back(pos.longitude);
96 }
97 }
98}
99
101 return FetchPositionImpl(std::stop_token{});
102}
103
104void ISSTracker::TrackingLoop(const std::stop_token& stopToken) {
105 while (!stopToken.stop_requested()) {
106 try {
107 const ISSPosition position = FetchPositionImpl(stopToken);
108
109 // Check if stop was requested after fetch (important for slow networks)
110 if (stopToken.stop_requested()) {
111 LOG_INFO("ISS Tracker: Stop requested, discarding fetched data");
112 break;
113 }
114
115 if (position.valid) {
116 // Update current position and add to history
117 {
118 const std::lock_guard<std::mutex> lock(m_dataMutex);
119 m_currentPosition = position;
120 AddToHistory(position);
121 }
122
123 // Invoke callback if set (copy under lock to avoid data race)
124 std::function<void(const ISSPosition&)> callback;
125 {
126 const std::lock_guard<std::mutex> lock(m_callbackMutex);
127 callback = m_callback;
128 }
129
130 if (callback) {
131 try {
132 callback(position);
133 } catch (const std::exception& e) {
134 LOG_ERROR("ISS Tracker: Callback threw exception: {}", e.what());
135 } catch (...) {
136 LOG_ERROR("ISS Tracker: Callback threw unknown exception");
137 }
138 }
139
140 LOG_INFO("ISS Tracker: Position updated - Lat: {}, Long: {}, Alt: {} km, Vel: {} km/h",
141 position.latitude, position.longitude, position.altitude, position.velocity);
142 }
143 } catch (const std::exception& e) {
144 LOG_ERROR("ISS Tracker: Error fetching position: {}", e.what());
145 } catch (...) {
146 LOG_ERROR("ISS Tracker: Unknown error fetching position");
147 }
148
149 // Wait 5 seconds before next update (if not stopped)
150 auto start = std::chrono::steady_clock::now();
151 while (!stopToken.stop_requested()) {
152 auto now = std::chrono::steady_clock::now();
153 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start).count();
154 if (elapsed >= 5) {
155 break;
156 }
157 std::this_thread::sleep_for(std::chrono::milliseconds(100));
158 }
159 }
160
161 LOG_INFO("ISS Tracker: Tracking loop exited");
162}
163
164ISSPosition ISSTracker::FetchPositionImpl(const std::stop_token& stopToken) {
165 ISSPosition position;
166 position.valid = false;
167
168 try {
169 const HttpClient http;
170 // 30s timeout, two retries to ride out flaky mobile networks. The
171 // wheretheiss.at endpoint is public and unauthenticated.
172 const HttpRequest request{.url = ISS_API_URL,
173 .userAgent = "MetaImGUI-ISSTracker/1.0",
174 .timeout = std::chrono::seconds{30},
175 .maxRetries = 2};
176 const HttpResponse response = http.Get(request, stopToken);
177
178 if (response.status == HttpStatus::Cancelled) {
179 return position; // Caller checks stop_token after we return.
180 }
181 if (response.status != HttpStatus::Ok) {
182 LOG_ERROR("ISS Tracker: HTTP fetch failed (status={})", static_cast<int>(response.status));
183 return position;
184 }
185 if (response.body.empty()) {
186 LOG_ERROR("ISS Tracker: Empty response from server");
187 return position;
188 }
189
190 position = ParseJSON(response.body);
191 } catch (const std::bad_alloc& e) {
192 LOG_ERROR("ISS Tracker: Memory allocation failed: {}", e.what());
193 } catch (const std::exception& e) {
194 LOG_ERROR("ISS Tracker: Fetch failed: {}", e.what());
195 } catch (...) {
196 LOG_ERROR("ISS Tracker: Unknown error during fetch");
197 }
198
199 return position;
200}
201
202ISSPosition ISSTracker::ParseJSON(const std::string& jsonResponse) {
203 ISSPosition position;
204 position.valid = false;
205
206 try {
207 auto json = nlohmann::json::parse(jsonResponse);
208
209 // Parse required fields
210 if (json.contains("latitude") && json.contains("longitude")) {
211 position.latitude = json["latitude"].get<double>();
212 position.longitude = json["longitude"].get<double>();
213
214 // Parse optional fields
215 if (json.contains("altitude")) {
216 position.altitude = json["altitude"].get<double>();
217 }
218 if (json.contains("velocity")) {
219 position.velocity = json["velocity"].get<double>();
220 }
221 if (json.contains("timestamp")) {
222 position.timestamp = json["timestamp"].get<long>();
223 }
224
225 position.valid = true;
226 } else {
227 LOG_ERROR("ISS Tracker: Missing required fields in JSON response");
228 }
229 } catch (const nlohmann::json::parse_error& e) {
230 LOG_ERROR("ISS Tracker: JSON parse error: {}", e.what());
231 } catch (const nlohmann::json::type_error& e) {
232 LOG_ERROR("ISS Tracker: JSON type error: {}", e.what());
233 } catch (const std::exception& e) {
234 LOG_ERROR("ISS Tracker: Error parsing JSON: {}", e.what());
235 }
236
237 return position;
238}
239
240void ISSTracker::AddToHistory(const ISSPosition& position) {
241 // Note: Caller must hold m_dataMutex
242
243 if (!position.valid) {
244 return;
245 }
246
247 // Add to history
248 m_positionHistory.push_back(position);
249
250 // Keep only the last maxHistorySize positions
251 while (m_positionHistory.size() > m_maxHistorySize) {
252 m_positionHistory.pop_front();
253 }
254}
255
256} // namespace MetaImGUI
nlohmann::json json
#define LOG_INFO(...)
Definition Logger.h:166
#define LOG_ERROR(...)
Definition Logger.h:168
void GetPositionHistory(std::vector< double > &latitudes, std::vector< double > &longitudes) const
Get position history for orbit trail (thread-safe)
ISSPosition GetCurrentPosition() const
Get the current ISS position (thread-safe)
void StopTracking()
Stop tracking.
void StartTracking(std::function< void(const ISSPosition &)> callback=nullptr)
Start tracking ISS position asynchronously.
bool IsTracking() const
Check if tracking is active.
ISSPosition FetchPositionSync()
Manually fetch ISS position once (synchronous)
@ Cancelled
stop_token::stop_requested() fired during transfer
@ Ok
2xx response, body populated