diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 450c9482b..7248a9702 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -271,10 +271,17 @@ ADSBRxDetailsView::ADSBRxDetailsView( &text_frame_pos_even, &text_frame_pos_odd, &button_aircraft_details, - &button_see_map}); + &button_see_map, + &opt_map_list}); text_icao_address.set(entry_.icao_str); + opt_map_list.on_change = [this](size_t, uint32_t mf) { + map_filter = mf; + clear_map_markers(); // reset them + pos_history.clear(); // erase the trail + }; + button_aircraft_details.on_select = [this, &nav](Button&) { aircraft_details_view_ = nav.push(entry_); nav.set_on_pop([this]() { @@ -288,7 +295,7 @@ ADSBRxDetailsView::ADSBRxDetailsView( get_map_tag(entry_), entry_.pos.altitude, GeoPos::alt_unit::FEET, - GeoPos::spd_unit::HIDDEN, + GeoPos::spd_unit::KNOTS, entry_.pos.latitude, entry_.pos.longitude, entry_.velo.heading); @@ -312,8 +319,12 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) { // AC Details view is showing, nothing to update. } else if (geomap_view_) { // Map is showing, update the current item. + if (map_filter == 1) { + // add to map trail history + add_map_trail(entry); + } geomap_view_->update_tag(get_map_tag(entry_)); - geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.velo.speed); + geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.get_ground_speed()); } else { // Details is showing, update details. refresh_ui(); @@ -364,6 +375,51 @@ void ADSBRxDetailsView::get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* void ADSBRxDetailsView::clear_map_markers() { if (geomap_view_) geomap_view_->clear_markers(); + if (map_filter == 1) + refresh_trail_markers(); // re add trail +} + +void ADSBRxDetailsView::refresh_trail_markers() { + if (!geomap_view_) + return; + + geomap_view_->clear_markers(); + for (const auto& pos : pos_history) { + GeoMarker marker{}; + marker.lon = pos.lon; + marker.lat = pos.lat; + marker.angle = pos.heading; + marker.tag = ""; // No tag for trail points + uint8_t r, g, b; + get_altitude_color(pos.altitude, &r, &g, &b); + marker.color = Color(r, g, b); + geomap_view_->store_marker(marker); + } +} + +void ADSBRxDetailsView::add_map_trail(const AircraftRecentEntry& entry) { + if (!geomap_view_) + return; + if (entry.pos.pos_valid == false) + return; + + if (!pos_history.empty()) { + const auto& last_pos = pos_history.back(); + const float THRESH_DEG = 0.009f; + const float THRESH_SQ = THRESH_DEG * THRESH_DEG; // 0.00002025f + + float d_lat = entry.pos.latitude - last_pos.lat; + float d_lon = entry.pos.longitude - last_pos.lon; + if ((d_lat * d_lat) + (d_lon * d_lon) < THRESH_SQ) { + return; // Moved less than ~1000m, skip (rough estimate, varies with latitude but good enough for our purposes). This prevents adding too many points when the plane is circling or taxiing. + } + } + + // Only keep the last 30 positions in the trail. + if (pos_history.size() >= 30) + pos_history.erase(pos_history.begin()); + pos_history.push_back({entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude}); + refresh_trail_markers(); } bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) { @@ -371,6 +427,8 @@ bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) { if (!geomap_view_) return false; + if (map_filter == 1) return false; // this is the 'only me' option, so skip all others + GeoMarker marker{}; marker.lon = entry.pos.longitude; marker.lat = entry.pos.latitude; diff --git a/firmware/application/apps/ui_adsb_rx.hpp b/firmware/application/apps/ui_adsb_rx.hpp index 6f7299919..c9a6e0468 100644 --- a/firmware/application/apps/ui_adsb_rx.hpp +++ b/firmware/application/apps/ui_adsb_rx.hpp @@ -64,9 +64,9 @@ namespace ui { #define VEL_AIR_SUBSONIC 3 #define VEL_AIR_SUPERSONIC 4 -#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames -#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm -#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map +#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames +#define MARKER_UPDATE_SECONDS_OSM 8 // "other" map marker redraw interval for osm +#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map /* Thresholds (in seconds) that define the transition between ages. */ struct ADSBAgeLimit { @@ -150,6 +150,19 @@ struct AircraftRecentEntry { age = 0; } + int32_t get_ground_speed() const { + if (velo.valid == false) return 0; + if (velo.type == SPD_GND) + return velo.speed; + else if (velo.type == SPD_IAS) { + if (!pos.alt_valid) return velo.speed; // can't correct without altitude, so just return IAS + return (int32_t)(velo.speed * (1.0f + (0.02f * (pos.altitude / 1000.0f)))); + } else if (velo.type == SPD_TAS) + return velo.speed; // We don't know the wind speed + else + return 0; + } + void inc_age(int delta) { age += delta; @@ -208,50 +221,50 @@ class ADSBRxAircraftDetailsView : public View { private: Labels labels{ - {{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 2 * 16}, "Registration:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 3 * 16}, "Manufacturer:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 5 * 16}, "Model:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 7 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 8 * 16}, "Number of engines:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 9 * 16}, "Engine type:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 11 * 16}, "Owner:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 13 * 16}, "Operator:", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X(0), UI_POS_Y(1)}, "ICAO:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Registration:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(5)}, "Model:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(7)}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(8)}, "Number of engines:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(9)}, "Engine type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(11)}, "Owner:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(13)}, "Operator:", Theme::getInstance()->fg_light->foreground}}; Text text_icao_address{ - {5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(1), 6 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_registration{ - {13 * 8, 2 * 16, 8 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(13), UI_POS_Y(2), 8 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_manufacturer{ - {UI_POS_X(0), 4 * 16, 19 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(4), 19 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_model{ - {UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_type{ - {5 * 8, 7 * 16, 22 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(7), 22 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_number_of_engines{ - {18 * 8, 8 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(18), UI_POS_Y(8), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_engine_type{ - {UI_POS_X(0), 10 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(10), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_owner{ - {UI_POS_X(0), 12 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(12), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_operator{ - {UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)}, "-"}; Button button_close{ @@ -276,6 +289,8 @@ class ADSBRxDetailsView : public View { void clear_map_markers(); /* Adds a marker for the entry to the map. Returns true on success. */ bool add_map_marker(const AircraftRecentEntry& entry); + void add_map_trail(const AircraftRecentEntry& entry); + void refresh_trail_markers(); std::string title() const override { return "Details"; } @@ -298,57 +313,72 @@ class ADSBRxDetailsView : public View { // if removed from the recent entries list. AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key}; bool airline_checked{false}; + uint8_t map_filter{0}; // 0: all, 1: only this, set by opt_map_list. + struct PosHistory { + float lat; + float lon; + uint16_t heading; + int32_t altitude; + }; + std::vector pos_history{}; Labels labels{ - {{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground}, - {{13 * 8, 1 * 16}, "Callsign:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 2 * 16}, "Last seen:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 3 * 16}, "Airline:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 5 * 16}, "Country:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 13 * 16}, "Even position frame:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 15 * 16}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X(0), UI_POS_Y(0)}, "ICAO:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(13), UI_POS_Y(0)}, "Callsign:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(1)}, "Last seen:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Airline:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(4)}, "Country:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(7)}, "Map filter:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(13)}, "Even position frame:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(15)}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}}; Text text_icao_address{ - {5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(0), 6 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_callsign{ - {22 * 8, 1 * 16, 8 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(22), UI_POS_Y(0), 8 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_last_seen{ - {11 * 8, 2 * 16, 19 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(11), UI_POS_Y(1), 19 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_airline{ - {UI_POS_X(0), 4 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(3), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_country{ - {8 * 8, 5 * 16, 22 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(8), UI_POS_Y(4), 22 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_infos{ - {UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(5), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_info2{ - {UI_POS_X(0), 7 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_frame_pos_even{ - {UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_frame_pos_odd{ - {UI_POS_X(0), 16 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(16), screen_width, UI_POS_HEIGHT(1)}, "-"}; Button button_aircraft_details{ - {UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, + {UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "A/C details"}; + OptionsField opt_map_list{ + {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(7)}, + 12, + {{"All", 0}, + {"Only this", 1}}}; + Button button_see_map{ - {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, + {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "See on map"}; MessageHandlerRegistration message_handler_gps{ diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index cc1274217..30cdd034a 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -876,8 +876,10 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a geopos.set_report_change(true); geomap.set_angle(angle); - if (is_changed) geomap.move(lon_, lat_); - geomap.set_dirty(); + if (is_changed) { + geomap.move(lon_, lat_); + geomap.set_dirty(); + } } void GeoMapView::update_tag(const std::string tag) {