Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/core/theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand All @@ -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<const std::string &>().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);
Expand Down
8 changes: 8 additions & 0 deletions src/core/theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down
109 changes: 89 additions & 20 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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++)
{
Expand All @@ -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++)
{
Expand All @@ -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);
Expand Down
45 changes: 34 additions & 11 deletions src/ui/tools/tool_layers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
}

Expand Down
6 changes: 6 additions & 0 deletions src/ui/tools/tools_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
7 changes: 7 additions & 0 deletions themes/amber/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions themes/daylight/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions themes/default/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions themes/girlypop/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions themes/midnight/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions themes/paper/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading