diff --git a/README.md b/README.md index 4498d55..f411a5c 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ The following settings can be included in a `StarCharter` configuration file: * `chart_edge_line_width` - Line width to use when marking the edge of the chart. Default: 2.5. * `constellation_boundaries` - Boolean (0 or 1) indicating whether we draw constellation boundaries. Default: 1. * `constellation_boundary_col` - Colour to use when drawing constellation boundaries -* `constellation_highlight` - Optionally, highlight the boundary of one particular constellation, identified by a three-letter abbreviation. +* `constellation_highlight` - Optionally, highlight the boundary of particular constellations, identified by three-letter abbreviation, comma sepparated list. * `constellation_label_col` - Colour to use when writing constellation names * `constellation_label_size` - Relative font size to use when rendering constellation names. Default: 1. * `constellation_names` - Boolean (0 or 1) indicating whether we label the names of constellations @@ -463,6 +463,7 @@ Only the filename is compulsory. The fields have the following meanings: * `angular_height` - The number of degrees of altitude spanned by the horizon graphic (vertically). By default, this is calculated from the aspect ratio and angular width of the image, assuming the pixels at the centre of the image to be square. So, if the image is twice as wide as it is high, and the angular width is 360 degrees, its angular height will be 180 degrees. ## Change history +FORK version (28 June 2026) - Added the ability to highlight multiple constellations. Fixed bug of generating more than 5 charts in one configuration file **Version 10.0** (6 June 2026) - Switched default ephemeris from DE430 to DE440. Add support for horizon graphics. diff --git a/dataFetch.py b/dataFetch.py index 09d9506..1882ecb 100755 --- a/dataFetch.py +++ b/dataFetch.py @@ -212,12 +212,12 @@ def fetch_required_files(refresh: bool = False, never_refresh: bool = False, { 'url': 'https://ftp.lowell.edu/pub/elgb/astorb.dat.gz', 'destination': 'data/astorb.dat', - 'force_refresh': True + 'force_refresh': refresh # was True }, { 'url': 'https://www.minorplanetcenter.net/iau/MPCORB/AllCometEls.txt', 'destination': 'data/Soft00Cmt.txt', - 'force_refresh': True + 'force_refresh': refresh # was True }, # Definitions of constellation boundaries diff --git a/data_processing/milky_way_map/src/main.h b/data_processing/milky_way_map/src/main.h index a236abf..182f7c4 100644 --- a/data_processing/milky_way_map/src/main.h +++ b/data_processing/milky_way_map/src/main.h @@ -26,20 +26,18 @@ #include // Output 1 is for starcharts_svg -#define SMOOTH 16 /* Median smoothing of Axel Mellinger's panorama */ -#define RESOLUTION_1 (360./8000*(SMOOTH)/16) /* resolution of grid in degrees */ +#define SMOOTH 16 /* Median smoothing of Axel Mellinger's panorama */ +#define RESOLUTION_1 (360./8000*(SMOOTH)/16) /* resolution of grid in degrees */ -#define H_SIZE_MAP1 ((int)(360/(RESOLUTION_1))) -#define V_SIZE_MAP1 ((int)(180/(RESOLUTION_1))) +#define H_SIZE_MAP1 8000 +#define V_SIZE_MAP1 4000 -// Output2 is for HTML5 planetarium -#define RESOLUTION_2 (360./4096) /* resolution of grid in degrees */ - -#define H_SIZE_MAP2 ((int)(360/(RESOLUTION_2))) -#define V_SIZE_MAP2 ((int)(100/(RESOLUTION_2))) +#define RESOLUTION_2 (360./4096) /* resolution of grid in degrees */ +#define H_SIZE_MAP2 4096 +#define V_SIZE_MAP2 1137 typedef struct { - unsigned char C[V_SIZE_MAP1][H_SIZE_MAP1]; + unsigned char C[V_SIZE_MAP1][H_SIZE_MAP1]; /* 4000 x 8000 = 32 MB */ } dataArray; #endif diff --git a/data_processing/star_catalogue_merged/main_catalogue_merge.py b/data_processing/star_catalogue_merged/main_catalogue_merge.py index cfb9d9d..098a6d8 100755 --- a/data_processing/star_catalogue_merged/main_catalogue_merge.py +++ b/data_processing/star_catalogue_merged/main_catalogue_merge.py @@ -1247,11 +1247,11 @@ def read_english_names() -> Tuple[StarList, str]: star: StarDescriptor = StarDescriptor() try: star.names_hip_num = int(line[90:].split()[0]) - except ValueError: + except (ValueError, IndexError): pass try: star.names_hd_num = int(line[97:].split()[0]) - except ValueError: + except (ValueError, IndexError): pass star.add_english_name(new_name=line[18:36].strip(), prepend=True) diff --git a/setup.sh b/setup.sh index 5436149..45b0277 100755 --- a/setup.sh +++ b/setup.sh @@ -112,9 +112,9 @@ cd ${cwd} || exit 1 # Make some test charts of the paths of solar system objects, to ensure all ASCII input ephemeris files are converted # to binary for rapid access on subsequent queries -echo "[`date`] Generating test charts" -cd ${cwd} || exit 1 -./runExamples.py "$@" || exit 1 +# echo "[`date`] Generating test charts" +#cd ${cwd} || exit 1 +#./runExamples.py "$@" || exit 1 # Finished cd ${cwd} || exit 1 diff --git a/src/astroGraphics/constellations.c b/src/astroGraphics/constellations.c index 4e94b2f..8e3c76e 100644 --- a/src/astroGraphics/constellations.c +++ b/src/astroGraphics/constellations.c @@ -85,8 +85,27 @@ void plot_constellation_boundaries(chart_config *s, line_drawer *ld) { double x_first = 0, y_first = 0; char line[FNAME_LENGTH], *scan, constellation[6] = "@@@@"; - // This must be set to true initially, to ensure that colour is set when we start tracing the first constellation - int was_highlighted = 1; + // Parse constellation_highlight into a list of 3-letter abbreviations, comma-separated + // e.g. "Ori,Tau,Gem" + // char highlight_buf[FNAME_LENGTH]; + // strncpy(highlight_buf, s->constellation_highlight, FNAME_LENGTH - 1); + // highlight_buf[FNAME_LENGTH - 1] = '\0'; + + char highlight_buf[FNAME_LENGTH]; + snprintf(highlight_buf, FNAME_LENGTH, "%s", s->constellation_highlight); + + char highlights[64][4]; // up to 64 highlighted constellations, 3 chars + null + int highlight_count = 0; + + char *token = strtok(highlight_buf, ","); + while (token != NULL && highlight_count < 64) { + // Strip leading/trailing spaces + while (token[0] == ' ') token++; + strncpy(highlights[highlight_count], token, 3); + highlights[highlight_count][3] = '\0'; + highlight_count++; + token = strtok(NULL, ","); + } // Set up line-drawing class ld_pen_up(ld, GSL_NAN, GSL_NAN, NULL, 1); @@ -137,22 +156,30 @@ void plot_constellation_boundaries(chart_config *s, line_drawer *ld) { x_first = x; y_first = y; + // Check if this constellation is in the highlight list + int is_highlighted = 0; + for (int i = 0; i < highlight_count; i++) { + if (strncasecmp(constellation, highlights[i], 3) == 0) { + is_highlighted = 1; + break; + } + } + // Set the line colour and width for the boundary of this constellation - if (strncmp(constellation, s->constellation_highlight, 3) == 0) { + if (is_highlighted) { cairo_set_source_rgba(s->cairo_draw, s->star_col.red, s->star_col.grn, s->star_col.blu, s->star_col.alpha); cairo_set_line_width(s->cairo_draw, 2 * s->line_width_base); - was_highlighted = 1; - } else if (was_highlighted) { + } else { cairo_set_source_rgba(s->cairo_draw, s->constellation_boundary_col.red, s->constellation_boundary_col.grn, s->constellation_boundary_col.blu, s->constellation_boundary_col.alpha); cairo_set_line_width(s->cairo_draw, 0.8 * s->line_width_base); - was_highlighted = 0; } + } ld_point(ld, x, y, NULL); @@ -162,6 +189,7 @@ void plot_constellation_boundaries(chart_config *s, line_drawer *ld) { ld_pen_up(ld, GSL_NAN, GSL_NAN, NULL, 1); } + //! plot_constellation_sticks - Draw stick figures to represent the constellations. //! \param s - A structure defining the properties of the star chart to be drawn. //! \param ld - A structure used to draw lines on a cairo surface. diff --git a/src/astroGraphics/deepSkyOutlines.c b/src/astroGraphics/deepSkyOutlines.c index 25141fc..4ead54e 100644 --- a/src/astroGraphics/deepSkyOutlines.c +++ b/src/astroGraphics/deepSkyOutlines.c @@ -1,7 +1,7 @@ // deepSkyOutlines.c // // ------------------------------------------------- -// Copyright 2015-2026 Dominic Ford +// Copyright 2015-2025 Dominic Ford // // This file is part of StarCharter. // @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -33,6 +34,33 @@ #include "settings/chart_config.h" #include "vectorGraphics/cairo_page.h" +#define DSO_MAX_POINTS 4096 +#define DSO_MAX_OUTLINES 4096 + +//! DsoPoint - A single point in a deep sky object outline +typedef struct { + double ra; // degrees + double dec; // degrees + int continuous; // 1 if this point continues from the previous, 0 if it starts a new sub-path +} DsoPoint; + +//! DsoOutline - All points belonging to one deep sky object outline +typedef struct { + DsoPoint points[DSO_MAX_POINTS]; + int point_count; +} DsoOutline; + +//! DsoOutlineCollection - The full set of loaded outlines +typedef struct { + DsoOutline outlines[DSO_MAX_OUTLINES]; + int outline_count; + int loaded; +} DsoOutlineCollection; + +// Global collection, loaded once at startup +static DsoOutlineCollection dso_collection = { .loaded = 0, .outline_count = 0 }; + + //! close_dso_outline - Close the path around a deep sky object //! \param s - A structure defining the properties of the star chart to be drawn. @@ -48,135 +76,164 @@ void close_dso_outline(chart_config *s) { cairo_stroke(s->cairo_draw); } -//! plot_deep_sky_outlines - Draw outlines of deep sky objects onto a star chart -//! \param s - A structure defining the properties of the star chart to be drawn. -//! \param page - A structure defining the cairo drawing context. -void plot_deep_sky_outlines(chart_config *s, cairo_page *page) { +//! load_dso_outlines - Load all deep sky object outline files into memory. +//! Should be called once before any charts are rendered. + +void load_dso_outlines() { + if (dso_collection.loaded) return; + dso_collection.loaded = 1; + dso_collection.outline_count = 0; + wordexp_t w; glob_t g; - // We only plot DSO outlines in the 'coloured' plot style - if (s->dso_style == SW_DSO_STYLE_FUZZY) return; - - // Path to where deep sky object outlines are stored const char *outlines_path = SRCDIR "/../data/deepSky/ngc/outlines/*.txt"; - // Fetch list of all the deep sky object outlines we have - if (wordexp(outlines_path, &w, 0) != 0) return; // No matches; return empty list + if (wordexp(outlines_path, &w, 0) != 0) return; + for (int i = 0; i < w.we_wordc; i++) { if (glob(w.we_wordv[i], 0, NULL, &g) != 0) continue; + for (int j = 0; j < g.gl_pathc; j++) { - // Deep sky object outline we are processing + if (dso_collection.outline_count >= DSO_MAX_OUTLINES) { + stch_log("Warning: DSO_MAX_OUTLINES reached; some outlines will not be loaded."); + break; + } + const char *outline_file = g.gl_pathv[j]; - // Logging message if (DEBUG) { char message[FNAME_LENGTH]; - snprintf(message, FNAME_LENGTH, "Drawing outline from <%s>", outline_file); + snprintf(message, FNAME_LENGTH, "Loading DSO outline from <%s>", outline_file); stch_log(message); } - // Open file FILE *file = fopen(outline_file, "r"); - if (file == NULL) stch_fatal(__FILE__, __LINE__, "Could not open deep sky object outline"); - - // Initially, read path into an array - const int max_points = 4096; - double ra[max_points], dec[max_points]; - double x[max_points], y[max_points]; - double x_canvas[max_points], y_canvas[max_points]; - int continuous[max_points]; - - // Loop over the lines of the data file - int point_counter = 0; - int reject_object = 0; + if (file == NULL) { + char message[FNAME_LENGTH]; + snprintf(message, FNAME_LENGTH, "Could not open deep sky object outline <%s>; skipping.", outline_file); + stch_log(message); + continue; + } + + DsoOutline *outline = &dso_collection.outlines[dso_collection.outline_count]; + outline->point_count = 0; + while ((!feof(file)) && (!ferror(file))) { + if (outline->point_count >= DSO_MAX_POINTS) break; + char line[FNAME_LENGTH]; const char *line_ptr = line; - file_readline(file, line, sizeof line); - // Ignore comment lines if (line[0] != 'l') continue; - // Extract data from line + DsoPoint *pt = &outline->points[outline->point_count]; + line_ptr = next_word(line_ptr); - continuous[point_counter] = (line_ptr[0] == '+'); + pt->continuous = (line_ptr[0] == '+'); line_ptr = next_word(line_ptr); - ra[point_counter] = get_float(line_ptr, NULL); + pt->ra = get_float(line_ptr, NULL); line_ptr = next_word(line_ptr); - dec[point_counter] = get_float(line_ptr, NULL); + pt->dec = get_float(line_ptr, NULL); - // Project RA and Dec of object into physical coordinates on the star chart - plane_project(&x[point_counter], &y[point_counter], s, - ra[point_counter] * M_PI / 180, dec[point_counter] * M_PI / 180, 0); + outline->point_count++; + } - // Reject this object if it falls outside the plot area - if ((!gsl_finite(x[point_counter])) || (!gsl_finite(y[point_counter]))) { - reject_object = 1; - break; - } + fclose(file); - if ( - (x[point_counter] < s->x_min * 1.2) || (x[point_counter] > s->x_max * 1.2) || - (y[point_counter] < s->y_min * 1.2) || (y[point_counter] > s->y_max * 1.2) - ) { - reject_object = 1; - break; - } + if (outline->point_count > 0) { + dso_collection.outline_count++; + } + } - // Convert coordinates from tangent plane into pixels on the Cairo canvas - fetch_canvas_coordinates(&x_canvas[point_counter], &y_canvas[point_counter], - x[point_counter], y[point_counter], s); + globfree(&g); + } - // Check that we haven't jumped off one side of star chart, and on the other side - if (point_counter > 0) { - double line_length = hypot(y_canvas[point_counter - 1] - y_canvas[point_counter], - x_canvas[point_counter - 1] - x_canvas[point_counter]); - if (line_length > 100) reject_object = 1; - } + wordfree(&w); - // Update point counter - point_counter++; - } + if (DEBUG) { + char message[FNAME_LENGTH]; + snprintf(message, FNAME_LENGTH, "Loaded %d DSO outlines into memory.", dso_collection.outline_count); + stch_log(message); + } +} - // If this object has been rejected, ignore it - if (reject_object) continue; - // Start drawing a path around the outline of this object - cairo_new_path(s->cairo_draw); +//! plot_deep_sky_outlines - Draw outlines of deep sky objects onto a star chart. +//! Assumes load_dso_outlines() has already been called. +//! \param s - A structure defining the properties of the star chart to be drawn. +//! \param page - A structure defining the cairo drawing context. - // Loop over all the points in the path - int line_point_counter = 0; - for (int k = 0; k < point_counter; k++) { +void plot_deep_sky_outlines(chart_config *s, cairo_page *page) { - // Either continue an existing line, or start a new path - if (line_point_counter == 0) { - cairo_move_to(s->cairo_draw, x_canvas[k], y_canvas[k]); - } else { - cairo_line_to(s->cairo_draw, x_canvas[k], y_canvas[k]); - } + // We only plot DSO outlines in the 'coloured' plot style + if (s->dso_style == SW_DSO_STYLE_FUZZY) return; + + // Ensure outlines are loaded (safe to call multiple times) + load_dso_outlines(); + + // Iterate over all loaded outlines + for (int c = 0; c < dso_collection.outline_count; c++) { + DsoOutline *outline = &dso_collection.outlines[c]; + + // Project all points for this chart and check visibility + double x_canvas[DSO_MAX_POINTS], y_canvas[DSO_MAX_POINTS]; + int reject_object = 0; - // Close path, if requested - if (!continuous[k]) { - close_dso_outline(s); - line_point_counter = 0; + for (int k = 0; k < outline->point_count; k++) { + double x, y; + plane_project(&x, &y, s, + outline->points[k].ra * M_PI / 180, + outline->points[k].dec * M_PI / 180, 0); + + if ((!gsl_finite(x)) || (!gsl_finite(y))) { + reject_object = 1; + break; + } + + if ((x < s->x_min * 1.2) || (x > s->x_max * 1.2) || + (y < s->y_min * 1.2) || (y > s->y_max * 1.2)) { + reject_object = 1; + break; + } + + fetch_canvas_coordinates(&x_canvas[k], &y_canvas[k], x, y, s); + + if (k > 0) { + double line_length = hypot(y_canvas[k - 1] - y_canvas[k], + x_canvas[k - 1] - x_canvas[k]); + if (line_length > 100) { + reject_object = 1; + break; } + } + } + + if (reject_object) continue; - // Update point counter - line_point_counter++; + // Draw the outline + cairo_new_path(s->cairo_draw); + int line_point_counter = 0; + + for (int k = 0; k < outline->point_count; k++) { + if (line_point_counter == 0) { + cairo_move_to(s->cairo_draw, x_canvas[k], y_canvas[k]); + } else { + cairo_line_to(s->cairo_draw, x_canvas[k], y_canvas[k]); } - // Finally, stroke and fill path - if (line_point_counter > 0) { + if (!outline->points[k].continuous) { close_dso_outline(s); + line_point_counter = 0; } - // Close outline file - fclose(file); + line_point_counter++; + } + + if (line_point_counter > 0) { + close_dso_outline(s); } - globfree(&g); } - wordfree(&w); } diff --git a/src/settings/chart_config.h b/src/settings/chart_config.h index bf867c3..beacb35 100644 --- a/src/settings/chart_config.h +++ b/src/settings/chart_config.h @@ -357,8 +357,8 @@ typedef struct chart_config { //! Either SW_STICKS_IAU or SW_STICKS_REY or SW_STICKS_SIMPLIFIED int constellation_stick_design; - //! Optionally select a constellation to highlight - char constellation_highlight[8]; + //! Optionally select a series of constellations to highlight + char constellation_highlight[FNAME_LENGTH]; //! Boolean indicating whether we label the English names of stars int star_names; diff --git a/src/settings/read_config.c b/src/settings/read_config.c index a45f0ec..173c5b4 100644 --- a/src/settings/read_config.c +++ b/src/settings/read_config.c @@ -867,10 +867,9 @@ int process_configuration_file_line(char *line, const char *filename, const int } return 0; } else if (strcmp(key, "constellation_highlight") == 0) { - //! constellation_highlight - Optionally highlight the boundary of a particular constellation, referenced - //! by its three-letter abbreviation - snprintf(x->constellation_highlight, 6, "%s", key_val); - x->constellation_highlight[6] = '\0'; + //! constellation_highlight - Optionally highlight the boundary of the constellations, referenced + //! by comma separated list of three-letter abbreviations + snprintf(x->constellation_highlight, FNAME_LENGTH, "%s", key_val); return 0; } else if (strcmp(key, "plot_stars") == 0) { //! plot_stars - Boolean (0 or 1) indicating whether we plot any stars