diff --git a/src/core/theme.cpp b/src/core/theme.cpp index 8cc95fb..87532c9 100644 --- a/src/core/theme.cpp +++ b/src/core/theme.cpp @@ -111,6 +111,16 @@ void ThemeInitDefaults(Theme *t) t->world.footprint_fill = ParseHexColor("#FFFFFF22", WHITE); t->world.footprint_border = ParseHexColor("#FFFFFF88", WHITE); + // multi-satellite future ground-track palette + t->ground_tracks.palette[0] = ParseHexColor("#66CCFFFF", SKYBLUE); + t->ground_tracks.palette[1] = ParseHexColor("#66FF66FF", GREEN); + t->ground_tracks.palette[2] = ParseHexColor("#FFAA00FF", ORANGE); + t->ground_tracks.palette[3] = ParseHexColor("#FF66CCFF", MAGENTA); + t->ground_tracks.palette[4] = ParseHexColor("#AA66FFFF", PURPLE); + t->ground_tracks.palette[5] = ParseHexColor("#FF6666FF", RED); + t->ground_tracks.palette[6] = ParseHexColor("#66A3FFFF", BLUE); + t->ground_tracks.palette[7] = ParseHexColor("#2DD4BFFF", GREEN); + // compact semantic UI palette (match themes/default/theme.json) t->ui.text = ParseHexColor("#FFFFFFFF", WHITE); t->ui.text_dim = ParseHexColor("#D3D3D3FF", LIGHTGRAY); @@ -209,6 +219,7 @@ bool ThemeLoad(const char *theme_name, Theme *t) const nlohmann::json root_meta = root.value("meta", nlohmann::json::object()); const nlohmann::json root_world = root.value("world", nlohmann::json::object()); + const nlohmann::json root_ground_tracks = root.value("ground_tracks", nlohmann::json::object()); const nlohmann::json root_ui = root.value("ui", nlohmann::json::object()); const nlohmann::json root_style = root.value("style", nlohmann::json::object()); const nlohmann::json root_font = root.value("font", nlohmann::json::object()); @@ -232,6 +243,22 @@ bool ThemeLoad(const char *theme_name, Theme *t) ParseColorField(root_world, "footprint_fill", &t->world.footprint_fill, t->world.footprint_fill); ParseColorField(root_world, "footprint_border", &t->world.footprint_border, t->world.footprint_border); + // multi-satellite future ground-track palette + auto ground_track_palette = root_ground_tracks.find("palette"); + if (ground_track_palette != root_ground_tracks.end() && ground_track_palette->is_array()) + { + for (int i = 0; i < GROUND_TRACK_PALETTE_SIZE && + i < (int)ground_track_palette->size(); i++) + { + if (ground_track_palette->at(i).is_string()) + { + t->ground_tracks.palette[i] = ParseHexColor( + ground_track_palette->at(i).get_ref().c_str(), + t->ground_tracks.palette[i]); + } + } + } + // compact UI palette ParseColorField(root_ui, "text", &t->ui.text, t->ui.text); ParseColorField(root_ui, "text_dim", &t->ui.text_dim, t->ui.text_dim); diff --git a/src/core/theme.h b/src/core/theme.h index 18f2a65..cc131a4 100644 --- a/src/core/theme.h +++ b/src/core/theme.h @@ -27,6 +27,9 @@ /** mix ratio applied to derive an active/pressed state from a base color */ #define THEME_STATE_ACTIVE 0.18f +/** number of colors available for multi-satellite ground tracks */ +#define GROUND_TRACK_PALETTE_SIZE 8 + /** compact semantic UI palette (11 colors) */ typedef struct { @@ -120,6 +123,11 @@ typedef struct Color footprint_border; /* coverage footprint outline */ } world; + struct + { + Color palette[GROUND_TRACK_PALETTE_SIZE]; /* Multi track colors */ + } ground_tracks; + ThemeUIColors ui; ThemeStyle style; ThemeFont font; diff --git a/src/main.cpp b/src/main.cpp index 4657ded..0b7bd16 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -168,6 +168,12 @@ static Mesh GenEarthMesh(float radius, int slices, int rings) return mesh; } +/** Pick a theme-defined color for an additional 2D future ground track. */ +static Color MultiGroundTrackColor(int index) +{ + return g_theme.ground_tracks.palette[index % GROUND_TRACK_PALETTE_SIZE]; +} + /** render orbit lines in 3d space */ static void draw_orbit_3d(Satellite *sat, double current_epoch, bool is_highlighted, float alpha, int step) { @@ -1697,6 +1703,62 @@ int main(void) } } + /* Future ground tracks share a fixed propagation budget in Multi mode. + * The focused satellite keeps the original resolution; the remaining + * budget is split across a maximum of eight active tracks in total. */ + const bool future_orbits_enabled = + ToolSettingGetBool(&cfg, LAYERS_KEY_FUTURE_ORBITS, true); + const int future_orbits_mode = + ToolSettingGetInt(&cfg, LAYERS_KEY_FUTURE_ORBITS_MODE, + LAYERS_FUTURE_ORBITS_FOCUSED); + const bool future_orbits_multi = + future_orbits_mode == LAYERS_FUTURE_ORBITS_MULTI; + const bool focused_track_valid = + active_sat && active_sat->is_active && active_sat->mean_motion > 0.0 && + !(is_pov_mode && active_sat == selected_sat); + + const float future_orbit_span = fmaxf(cfg.orbits_to_draw, 0.25f); + const int requested_future_segments = + (int)fminf(4000.0f, fmaxf(50.0f, 400.0f * future_orbit_span)); + const int future_segment_budget = 6000; + int extra_track_limit = future_orbits_multi + ? LAYERS_FUTURE_ORBITS_MAX_TRACKS - (focused_track_valid ? 1 : 0) + : 0; + if (extra_track_limit < 0) + extra_track_limit = 0; + + int extra_track_count = 0; + if (future_orbits_enabled && future_orbits_multi) + { + for (int i = 0; i < sat_count && extra_track_count < extra_track_limit; i++) + { + const Satellite *sat = &satellites[i]; + if (!sat->is_active || sat->mean_motion <= 0.0 || sat == active_sat) + continue; + if (is_pov_mode && sat == selected_sat) + continue; + extra_track_count++; + } + } + + int extra_future_segments = requested_future_segments; + if (extra_track_count > 0) + { + const int focused_cost = focused_track_valid ? requested_future_segments : 0; + const int remaining_budget = future_segment_budget - focused_cost; + extra_future_segments = remaining_budget / extra_track_count; + if (extra_future_segments > requested_future_segments) + extra_future_segments = requested_future_segments; + if (extra_future_segments < 50) + extra_future_segments = 50; + } + + Vector3 future_sun_dir = {0}; + if (future_orbits_enabled && cfg.highlight_sunlit) + future_sun_dir = Vector3Normalize(calculate_sun_position(current_epoch)); + + int extra_tracks_drawn = 0; + /* render all satellites on 2d map */ for (int i = 0; i < sat_count; i++) { @@ -1711,20 +1773,33 @@ int main(void) Color sCol = (selected_sat == &satellites[i]) ? g_theme.world.sat_selected : (hovered_sat == &satellites[i]) ? g_theme.world.sat_hover : g_theme.world.sat; sCol = ApplyAlpha(sCol, sat_alpha); - if (is_hl && !(is_pov_mode && &satellites[i] == selected_sat) && ToolSettingGetBool(&cfg, LAYERS_KEY_FUTURE_ORBITS, true)) + bool draw_future_track = false; + if (future_orbits_enabled && satellites[i].mean_motion > 0.0 && + !(is_pov_mode && &satellites[i] == selected_sat)) + { + if (is_hl) + { + draw_future_track = true; + } + else if (future_orbits_multi && extra_tracks_drawn < extra_track_count) + { + extra_tracks_drawn++; + draw_future_track = true; + } + } + + if (draw_future_track) { - int segments = fmin(4000, fmax(50, (int)(400 * cfg.orbits_to_draw))); + const int segments = is_hl ? requested_future_segments : extra_future_segments; Vector2 track_pts[4001]; bool is_sunlit_arr[4001]; - double period_days = (2.0 * PI / satellites[i].mean_motion) / 86400.0; - double time_step = (period_days * cfg.orbits_to_draw) / segments; + const Color track_color = ApplyAlpha( + is_hl ? g_theme.world.orbit_active : MultiGroundTrackColor(i), + sat_alpha); - Vector3 base_sun_dir = {0}; - if (cfg.highlight_sunlit) - { - base_sun_dir = Vector3Normalize(calculate_sun_position(current_epoch)); - } + double period_days = (2.0 * PI / satellites[i].mean_motion) / 86400.0; + double time_step = (period_days * future_orbit_span) / segments; for (int j = 0; j <= segments; j++) { @@ -1735,31 +1810,25 @@ int main(void) if (cfg.highlight_sunlit) { - is_sunlit_arr[j] = !is_sat_eclipsed(raw_pos, base_sun_dir); + is_sunlit_arr[j] = !is_sat_eclipsed(raw_pos, future_sun_dir); } } for (int offset_i = -1; offset_i <= 1; offset_i++) { float x_off = offset_i * map_w; - bool is_selected = (selected_sat == &satellites[i]); for (int j = 1; j <= segments; j++) { if (fabs(track_pts[j].x - track_pts[j - 1].x) < map_w * 0.6f) { - Color drawCol = ApplyAlpha(is_hl ? g_theme.world.orbit_active : g_theme.world.orbit, sat_alpha); - if (cfg.highlight_sunlit) - { - if (is_sunlit_arr[j]) - drawCol = ApplyAlpha(g_theme.world.sat_hover, sat_alpha); - else - drawCol = ApplyAlpha(is_hl ? g_theme.world.orbit_active : g_theme.world.orbit, sat_alpha); - } + Color drawCol = track_color; + if (cfg.highlight_sunlit && is_sunlit_arr[j]) + drawCol = ApplyAlpha(g_theme.world.sat_hover, sat_alpha); DrawLineEx((Vector2){track_pts[j - 1].x + x_off, track_pts[j - 1].y}, (Vector2){track_pts[j].x + x_off, track_pts[j].y}, 2.0f / Camera2DParams.zoom, drawCol); } } - if (cfg.show_apsides) + if (cfg.show_apsides && is_hl) { Vector2 peri2d, apo2d; get_apsis_2d(&satellites[i], current_epoch, false, gmst_deg, cfg.earth_rotation_offset, map_w, map_h, &peri2d); diff --git a/src/ui/tools/tool_layers.cpp b/src/ui/tools/tool_layers.cpp index b895788..9b35aaa 100644 --- a/src/ui/tools/tool_layers.cpp +++ b/src/ui/tools/tool_layers.cpp @@ -218,10 +218,10 @@ void DrawPanelLayers(UIContext *ctx, AppConfig *cfg) if (is_2d) { - /* Future Orbits: master toggle + slider, styled to match the other layer rows (icon cell + label) */ + /* Future Orbits: master toggle + focused/multi scope + orbit span. */ bool future_orbits_enabled = ToolSettingGetBool(cfg, LAYERS_KEY_FUTURE_ORBITS, true); bool future_orbits_prev = future_orbits_enabled; - float future_row_avail = ImGui::GetContentRegionAvail().x; /* full row width, for right-aligning the slider */ + float future_row_avail = ImGui::GetContentRegionAvail().x; ImGui::PushStyleColor(ImGuiCol_Text, ThemeColor(future_orbits_enabled ? g_theme.ui.accent : g_theme.ui.text_dim)); ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); @@ -234,20 +234,43 @@ void DrawPanelLayers(UIContext *ctx, AppConfig *cfg) ImGui::SameLine(); ImGui::Checkbox("Future Orbits", &future_orbits_enabled); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Show the predicted future orbit track on the 2D map"); + ImGui::SetTooltip("Show predicted future ground tracks on the 2D map"); if (future_orbits_enabled != future_orbits_prev) ToolSettingSetBool(cfg, LAYERS_KEY_FUTURE_ORBITS, future_orbits_enabled); - /* right-align the slider on the row, like the Labels Sel/All dropdown above */ - const float slider_w = 140.0f; - ImGui::SameLine(future_row_avail - slider_w); - ImGui::SetNextItemWidth(slider_w); ImGui::BeginDisabled(!future_orbits_enabled); - float future_orbits = cfg->orbits_to_draw; - if (ImGui::SliderFloat("##future_orbits", &future_orbits, 0.25f, 10.0f, "%.2f")) - cfg->orbits_to_draw = future_orbits; + + ImGui::SameLine(future_row_avail - combo_w); + ImGui::SetNextItemWidth(combo_w); + int future_mode = ToolSettingGetInt( + cfg, LAYERS_KEY_FUTURE_ORBITS_MODE, LAYERS_FUTURE_ORBITS_FOCUSED); + const char *future_modes[] = { "Sel", "Multi" }; + if (ImGui::Combo("##future_orbits_mode", &future_mode, future_modes, 2)) + ToolSettingSetInt(cfg, LAYERS_KEY_FUTURE_ORBITS_MODE, future_mode); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Number of predicted orbits shown on the 2D map"); + ImGui::SetTooltip( + "Sel: hovered satellite, otherwise selected satellite; " + "Multi: up to %d active satellite tracks total", + LAYERS_FUTURE_ORBITS_MAX_TRACKS); + + ImGui::Indent(icon_w); + int orbit_quarters = (int)roundf(cfg->orbits_to_draw * 4.0f); + if (orbit_quarters < 1) orbit_quarters = 1; + if (orbit_quarters > 40) orbit_quarters = 40; + + const float orbit_value_w = + ImGui::CalcTextSize("10.00").x + ImGui::GetStyle().ItemSpacing.x; + ImGui::SetNextItemWidth(future_row_avail - icon_w - orbit_value_w); + ImGui::SliderInt("##future_orbits", &orbit_quarters, 1, 40, ""); + const bool orbit_slider_hovered = ImGui::IsItemHovered(); + cfg->orbits_to_draw = orbit_quarters * 0.25f; + + ImGui::SameLine(); + ImGui::Text("%.2f", cfg->orbits_to_draw); + if (orbit_slider_hovered || ImGui::IsItemHovered()) + ImGui::SetTooltip("Number of predicted orbits shown for each future ground track (0.25-orbit steps)"); + ImGui::Unindent(icon_w); + ImGui::EndDisabled(); } diff --git a/src/ui/tools/tools_common.h b/src/ui/tools/tools_common.h index 6c73c69..0c5f2f9 100644 --- a/src/ui/tools/tools_common.h +++ b/src/ui/tools/tools_common.h @@ -21,6 +21,12 @@ extern bool log_show_timestamps; /* show timestamps in log entries (default o /* master switch for the 2D "Future Orbits" track (bool, default true) */ #define LAYERS_KEY_FUTURE_ORBITS "layers.future_orbits" +/* Future Orbits scope: focused track only, or a bounded multi-track set. */ +#define LAYERS_KEY_FUTURE_ORBITS_MODE "layers.future_orbits_mode" +#define LAYERS_FUTURE_ORBITS_FOCUSED 0 +#define LAYERS_FUTURE_ORBITS_MULTI 1 +#define LAYERS_FUTURE_ORBITS_MAX_TRACKS 8 + /* universal master switch for the Earth surface texture (bool, default true). * When off, the Earth renders as a plain black body in both the 2D map and the * 3D globe (see main.cpp). */ diff --git a/themes/amber/theme.json b/themes/amber/theme.json index 403f0ad..7077ebf 100644 --- a/themes/amber/theme.json +++ b/themes/amber/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#E8C98A22", "footprint_border": "#E8C98A88" }, + "ground_tracks": { + "palette": [ + "#66CCFFFF", "#34D399FF", "#F97316FF", + "#FB7185FF", "#C084FCFF", "#FACC15FF", + "#60A5FAFF", "#2DD4BFFF" + ] + }, "ui": { "text": "#F5E9D0FF", "text_dim": "#B8A88CFF", diff --git a/themes/daylight/theme.json b/themes/daylight/theme.json index e047a0e..02c37bb 100644 --- a/themes/daylight/theme.json +++ b/themes/daylight/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#1E3A5F22", "footprint_border": "#1E3A5F88" }, + "ground_tracks": { + "palette": [ + "#0369A1FF", "#059669FF", "#C2410CFF", + "#BE185DFF", "#7E22CEFF", "#B91C1CFF", + "#1D4ED8FF", "#0F766EFF" + ] + }, "ui": { "text": "#1F2937FF", "text_dim": "#6B7280FF", diff --git a/themes/default/theme.json b/themes/default/theme.json index 231062e..c57191a 100644 --- a/themes/default/theme.json +++ b/themes/default/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#FFFFFF22", "footprint_border": "#FFFFFF88" }, + "ground_tracks": { + "palette": [ + "#66CCFFFF", "#66FF66FF", "#FFAA00FF", + "#FF66CCFF", "#AA66FFFF", "#FF6666FF", + "#66A3FFFF", "#2DD4BFFF" + ] + }, "ui": { "text": "#FFFFFFFF", "text_dim": "#D3D3D3FF", diff --git a/themes/girlypop/theme.json b/themes/girlypop/theme.json index d4821f2..ce85bd3 100644 --- a/themes/girlypop/theme.json +++ b/themes/girlypop/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#E5E7EB22", "footprint_border": "#E5E7EB88" }, + "ground_tracks": { + "palette": [ + "#38BDF8FF", "#34D399FF", "#FB7185FF", + "#F472B6FF", "#C084FCFF", "#F97316FF", + "#818CF8FF", "#14B8A6FF" + ] + }, "ui": { "text": "#E5E7EBFF", "text_dim": "#9CA3AFFF", diff --git a/themes/midnight/theme.json b/themes/midnight/theme.json index 3f1169c..4ca74b2 100644 --- a/themes/midnight/theme.json +++ b/themes/midnight/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#8FA8D922", "footprint_border": "#8FA8D988" }, + "ground_tracks": { + "palette": [ + "#38BDF8FF", "#34D399FF", "#FB923CFF", + "#F472B6FF", "#A78BFAFF", "#F87171FF", + "#60A5FAFF", "#2DD4BFFF" + ] + }, "ui": { "text": "#E2E8F0FF", "text_dim": "#94A3B8FF", diff --git a/themes/paper/theme.json b/themes/paper/theme.json index 3ddef7b..b7863b8 100644 --- a/themes/paper/theme.json +++ b/themes/paper/theme.json @@ -17,6 +17,13 @@ "footprint_fill": "#5B463622", "footprint_border": "#5B463688" }, + "ground_tracks": { + "palette": [ + "#0369A1FF", "#15803DFF", "#C2410CFF", + "#BE185DFF", "#7E22CEFF", "#B91C1CFF", + "#0F766EFF", "#A16207FF" + ] + }, "ui": { "text": "#3B2F2FFF", "text_dim": "#7A6A5AFF",