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
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,18 @@ jobs:

- name: Install Code Quality Tools
run: |
sudo apt-get install -y cppcheck clang-format
sudo apt-get update

# Nonsense to get clang-format-22, which formats different than default 18
sudo apt-get install -y wget gnupg lsb-release software-properties-common
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo add-apt-repository "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-22 main"
sudo apt-get update
sudo apt-get install -y clang-format-22 cppcheck
sudo ln -sf /usr/bin/clang-format-22 /usr/bin/clang-format
hash -r
clang-format --version
clang-format --version

- name: Run Cppcheck
run: |
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ ignore:
# Keep main.c minimal and modularize logic into separate functions whenever possible.
- "src/main.c"
- "src/core_render.c"
- "include/arg_definitions.h"
fixes:
- "/home/runner/work/astroterm/astroterm/::"
488 changes: 283 additions & 205 deletions data/cities.csv

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions include/arg_definitions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* Contains definitions for command line arguments.
*
* Each line is an invocation of a macro whose arguments define a command line argument of a particular type.
*
* The macro arguments are:
* - The source-code variable name of the argument definition.
* - The short and long versions of the argument.
* - The type of the argument (where applicable).
* - The help text for the argument.
*
* To use these definitions, define the 4 macros below so that they use the arguments, then
* include this file in the source code.
*
* This approach is based on the idea of "X macros" (https://en.wikipedia.org/wiki/X_macro)
*
*/

#if !defined(INCLUDE_ARG_DEFINITION_DBL0)
#define INCLUDE_ARG_DEFINITION_DBL0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_STR0)
#define INCLUDE_ARG_DEFINITION_STR0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_LIT0)
#define INCLUDE_ARG_DEFINITION_LIT0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_INT0)
#define INCLUDE_ARG_DEFINITION_INT0(...)
#endif

INCLUDE_ARG_DEFINITION_DBL0(latitude_arg, "a", "latitude", "<degrees>", "Observer latitude [-90°, 90°] (default: 0.0)");
INCLUDE_ARG_DEFINITION_DBL0(longitude_arg, "o", "longitude", "<degrees>", "Observer longitude [-180°, 180°] (default: 0.0)");
INCLUDE_ARG_DEFINITION_DBL0(threshold_arg, "t", "threshold", "<float>",
"Only render stars brighter than this magnitude (default: 5.0)");
INCLUDE_ARG_DEFINITION_DBL0(label_arg, "l", "label-thresh", "<float>",
"Label stars brighter than this magnitude (default: 0.25)");
INCLUDE_ARG_DEFINITION_DBL0(speed_arg, "s", "speed", "<float>", "Animation speed multiplier (default: 1.0)");
INCLUDE_ARG_DEFINITION_DBL0(ratio_arg, "r", "aspect-ratio", "<float>",
"Override the calculated terminal cell aspect ratio. Use this if your projection is not 'square.' "
"A value around 2.0 works well for most cases");
INCLUDE_ARG_DEFINITION_STR0(datetime_arg, "d", "datetime", "<yyyy-mm-ddThh:mm:ss>", "Observation datetime in UTC");
INCLUDE_ARG_DEFINITION_STR0(
city_arg, "i", "city", "<city_name>",
"Use the latitude and longitude of the provided city. If the name contains multiple words, enclose the name in single or "
"double quotes. For a list of available cities, see: https://github.com/da-luce/astroterm/blob/v" PROJ_VERSION
"/data/cities.csv");
INCLUDE_ARG_DEFINITION_LIT0(color_arg, "c", "color", "Enable terminal colors");
INCLUDE_ARG_DEFINITION_LIT0(
constell_arg, "C", "constellations",
"Draw constellation stick figures. Note: a constellation is only drawn if all stars in the figure are over the threshold");
INCLUDE_ARG_DEFINITION_LIT0(grid_arg, "g", "grid", "Draw an azimuthal grid");
INCLUDE_ARG_DEFINITION_LIT0(unicode_arg, "u", "unicode", "Use unicode characters");
INCLUDE_ARG_DEFINITION_LIT0(braille_arg, "b", "braille", "Use braille characters for constellation lines (requires Unicode)");
INCLUDE_ARG_DEFINITION_LIT0(quit_arg, "q", "quit-on-any", "Quit on any keypress (default is to quit on 'q' or 'ESC' only)");
INCLUDE_ARG_DEFINITION_LIT0(meta_arg, "m", "metadata", "Display metadata");
INCLUDE_ARG_DEFINITION_LIT0(help_arg, "h", "help", "Print this help message");
INCLUDE_ARG_DEFINITION_LIT0(completions_arg, "B", "bash-completions", "Print bash completions");
INCLUDE_ARG_DEFINITION_LIT0(version_arg, "v", "version", "Display version info and exit");
INCLUDE_ARG_DEFINITION_INT0(fps_arg, "f", "fps", "<int>", "Frames per second (default: 24)");

#undef INCLUDE_ARG_DEFINITION_DBL0
#undef INCLUDE_ARG_DEFINITION_STR0
#undef INCLUDE_ARG_DEFINITION_LIT0
#undef INCLUDE_ARG_DEFINITION_INT0
4 changes: 4 additions & 0 deletions include/city.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ CityData *get_city(const char *name);
*/
void free_city(CityData *city);

/* Apply a callback and some associated data to all city definitions
*/
void iter_cities(void (*callback)(const CityData *city, void *data), void *data);

#endif // CITY_H
6 changes: 6 additions & 0 deletions include/split_lines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef SPLIT_LINES_H
#define SPLIT_LINES_H

char **split_lines(char *data, int *line_count_out);

#endif // SPLIT_LINES_H
6 changes: 6 additions & 0 deletions scripts/filter_cities.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ def process_cities(input_txt, output_csv, population_threshold=15000):
for row in reader:
try:
city_name = row[1].replace("’", "'")

# FIX: Handle the "Mianzhu, Deyang, Sichuan" anomaly
# by splitting at the comma and taking the first element
# (See PR #80)
city_name = city_name.split(',')[0].strip()

population = int(row[14])
country_code = row[8]
timezone = row[17]
Expand Down
173 changes: 117 additions & 56 deletions src/city.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "city.h"
#include "cities.h"
#include "macros.h"
#include "split_lines.h"

#include <ctype.h>
#include <stdint.h>
Expand Down Expand Up @@ -105,10 +106,6 @@ CityData *get_city(const char *name)
return NULL;
}

// Convert the byte array into an array of lines
char **lines = NULL;
size_t line_count = 0;

char *data = malloc(cities_len + 1);
if (data == NULL)
{
Expand All @@ -119,48 +116,129 @@ CityData *get_city(const char *name)
memcpy(data, cities, cities_len);
data[cities_len] = '\0';

const char *line = strtok(data, "\n");
while (line != NULL)
// Convert the byte array into an array of lines
int line_count = 0;
char **lines = split_lines(data, &line_count);
if (lines == NULL)
{
char *line_copy = strdup(line);
if (line_copy == NULL)
free(normalized_name);
free(data);
return NULL;
}

// Perform a linear search through the lines to find the best match
// (largest population) for the city name. E.g. London UK vs London Ontario.
CityData *best_city = NULL;
int best_population = -1;

for (int i = 0; i < line_count; i++)
{
char *line = strdup(lines[i]); // strtok modifies the string
if (!line)
continue;

char *city_name = strtok(line, ",");
char *population_str = strtok(NULL, ",");
char *country_code = strtok(NULL, ",");
strtok(NULL, ","); // timezone
char *latitude_str = strtok(NULL, ",");
char *longitude_str = strtok(NULL, ",");

if (!city_name || !population_str || !latitude_str || !longitude_str)
{
perror("Memory allocation failed");
free(normalized_name);
free(data);
return NULL;
free(line);
continue;
}

char **temp = realloc(lines, (line_count + 1) * sizeof(char *));
if (temp == NULL)
char *current_normalized = normalize_city_name(city_name);

// Check for match
if (current_normalized && strcmp(normalized_name, current_normalized) == 0)
{
perror("Memory allocation failed");
free(normalized_name);
free(data);
for (size_t i = 0; i < line_count; i++)
int population = atoi(population_str);
if (population > best_population)
{
free(lines[i]);
best_population = population;

// Free previous best
if (best_city)
{
free(best_city->city_name);
free(best_city);
}

best_city = malloc(sizeof(CityData));
if (best_city)
{
best_city->city_name = strdup(city_name);
best_city->latitude = atof(latitude_str);
best_city->longitude = atof(longitude_str);
}
}
free(lines);
return NULL;
}
lines = temp;

lines[line_count++] = line_copy;
line = strtok(NULL, "\n");
free(current_normalized);
free(line);
}

// Clean up
for (int i = 0; i < line_count; i++)
{
free(lines[i]);
}
free(lines);
free(data);
free(normalized_name);

return best_city;
}

void free_city(CityData *city)
{
if (city != NULL)
{
free((void *)city->city_name);
free(city);
}
}

/**
* @brief Iterates over all cities and applies a callback function to each.
*
* This function traverses the collection of cities and calls the provided
* callback function for each city. The callback receives a pointer to the
* city's data and a user-defined data pointer.
*
* @param callback A function pointer to be called for each city.
* @param user_data A pointer to user-defined data that will be passed to the
* callback function for each city.
*/
void iter_cities(void (*callback)(const CityData *city, void *data), void *user_data)
{
if (callback == NULL)
{
return;
}

// Perform binary search with the normalized name
char **result = bsearch(normalized_name, lines, line_count, sizeof(char *), compare_city);
// Get the city data as a buffer using split_lines
char *data = malloc(cities_len + 1);
memcpy(data, cities, cities_len);
data[cities_len] = '\0';

CityData *city = NULL;
if (result != NULL)
int line_count = 0;
char **lines = split_lines(data, &line_count);
if (lines == NULL)
{
// Parse the line for city data
char *matched_line = *result;
free(data);
return;
}

for (int i = 1; i < line_count; i++)
{
char *line = lines[i];
const char *city_name, *latitude_str, *longitude_str;

city_name = strtok(matched_line, ",");
city_name = strtok(line, ",");
strtok(NULL, ","); // Skip population
strtok(NULL, ","); // Skip country code
strtok(NULL, ","); // Skip timezone
Expand All @@ -169,37 +247,20 @@ CityData *get_city(const char *name)

if (city_name && latitude_str && longitude_str)
{
city = malloc(sizeof(CityData));
if (city == NULL)
{
perror("Memory allocation failed");
}
else
{
city->city_name = strdup(city_name);
city->latitude = atof(latitude_str);
city->longitude = atof(longitude_str);
}
CityData city_data;
city_data.city_name = city_name;
city_data.latitude = atof(latitude_str);
city_data.longitude = atof(longitude_str);

callback(&city_data, user_data);
}
}

// Clean up lines and data
for (size_t i = 0; i < line_count; i++)
// Clean up
for (int i = 0; i < line_count; i++)
{
free(lines[i]);
}
free(lines);
free(data);
free(normalized_name);

return city;
}

void free_city(CityData *city)
{
if (city != NULL)
{
free((void *)city->city_name);
free(city);
}
}
18 changes: 9 additions & 9 deletions src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,17 @@ bool generate_planet_table(struct Planet **planet_table, const struct KepElems *
return false;
}

const char *planet_symbols_unicode[NUM_PLANETS] = {
[SUN] = "☉", [MERCURY] = "☿", [VENUS] = "", [EARTH] = "🜨", [MARS] = "",
[JUPITER] = "♃", [SATURN] = "♄", [URANUS] = "⛢", [NEPTUNE] = "♆"};
const char *planet_symbols_unicode[NUM_PLANETS] = {[SUN] = "☉", [MERCURY] = "☿", [VENUS] = "♀",
[EARTH] = "🜨", [MARS] = "", [JUPITER] = "",
[SATURN] = "♄", [URANUS] = "⛢", [NEPTUNE] = "♆"};

const char planet_symbols_ASCII[NUM_PLANETS] = {
[SUN] = '@', [MERCURY] = '*', [VENUS] = '*', [EARTH] = '*', [MARS] = '*',
[JUPITER] = '*', [SATURN] = '*', [URANUS] = '*', [NEPTUNE] = '*'};
const char planet_symbols_ASCII[NUM_PLANETS] = {[SUN] = '@', [MERCURY] = '*', [VENUS] = '*',
[EARTH] = '*', [MARS] = '*', [JUPITER] = '*',
[SATURN] = '*', [URANUS] = '*', [NEPTUNE] = '*'};

const char *planet_labels[NUM_PLANETS] = {
[SUN] = "Sun", [MERCURY] = "Mercury", [VENUS] = "Venus", [EARTH] = "Earth", [MARS] = "Mars",
[JUPITER] = "Jupiter", [SATURN] = "Saturn", [URANUS] = "Uranus", [NEPTUNE] = "Neptune"};
const char *planet_labels[NUM_PLANETS] = {[SUN] = "Sun", [MERCURY] = "Mercury", [VENUS] = "Venus",
[EARTH] = "Earth", [MARS] = "Mars", [JUPITER] = "Jupiter",
[SATURN] = "Saturn", [URANUS] = "Uranus", [NEPTUNE] = "Neptune"};

// TODO: find better way to map these values
const int planet_colors[NUM_PLANETS] = {
Expand Down
Loading