diff --git a/general/g.parser/standard_option.c b/general/g.parser/standard_option.c index 12c08f96f2e..14c816a0e18 100644 --- a/general/g.parser/standard_option.c +++ b/general/g.parser/standard_option.c @@ -55,6 +55,7 @@ static char *STD_OPT_STRINGS[] = {"G_OPT_UNDEFINED", "G_OPT_F_SEP", "G_OPT_C", "G_OPT_CN", + "G_OPT_C_FORMAT", "G_OPT_M_UNITS", "G_OPT_M_DATATYPE", "G_OPT_M_MAPSET", diff --git a/include/grass/gis.h b/include/grass/gis.h index c032b40de06..fcc53535f47 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -316,8 +316,9 @@ typedef enum { G_OPT_F_OUTPUT, /*!< new output file */ G_OPT_F_SEP, /*!< data field separator */ - G_OPT_C, /*!< color */ - G_OPT_CN, /*!< color or none */ + G_OPT_C, /*!< color */ + G_OPT_CN, /*!< color or none */ + G_OPT_C_FORMAT, /*!< set color format to rgb,hex,hsv or triplet */ G_OPT_M_UNITS, /*!< units */ G_OPT_M_DATATYPE, /*!< datatype */ diff --git a/lib/gis/parser_standard_options.c b/lib/gis/parser_standard_options.c index 5c3da23c7bf..2addde624bf 100644 --- a/lib/gis/parser_standard_options.c +++ b/lib/gis/parser_standard_options.c @@ -97,6 +97,7 @@ - colors - G_OPT_C - G_OPT_CN + - G_OPT_C_FORMAT - misc - G_OPT_M_DIR @@ -652,6 +653,22 @@ struct Option *G_define_standard_option(int opt) Opt->description = _("Either a standard color name, R:G:B triplet, or \"none\""); break; + case G_OPT_C_FORMAT: + Opt->key = "color_format"; + Opt->type = TYPE_STRING; + Opt->key_desc = "name"; + Opt->required = YES; + Opt->multiple = NO; + Opt->answer = "hex"; + Opt->options = "rgb,hex,hsv,triplet"; + Opt->label = _("Color format"); + Opt->description = _("Color format for output values."); + G_asprintf( + (char **)&(Opt->descriptions), "rgb;%s;hex;%s;hsv;%s;triplet;%s", + _("output color in RGB format"), _("output color in HEX format"), + _("output color in HSV format (experimental)"), + _("output color in colon-separated RGB format")); + break; /* misc */ diff --git a/raster/r.colors.out/Makefile b/raster/r.colors.out/Makefile index b6717d12937..e2acf03ca41 100644 --- a/raster/r.colors.out/Makefile +++ b/raster/r.colors.out/Makefile @@ -1,13 +1,13 @@ MODULE_TOPDIR = ../.. -LIBES2 = $(RASTERLIB) $(GISLIB) -LIBES3 = $(RASTER3DLIB) $(RASTERLIB) $(GISLIB) +LIBES2 = $(RASTERLIB) $(GISLIB) $(PARSONLIB) +LIBES3 = $(RASTER3DLIB) $(RASTERLIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(RASTER3DDEP) $(GISDEP) $(RASTERDEP) PROGRAMS = r.colors.out r3.colors.out -r_colors_out_OBJS = raster_main.o -r3_colors_out_OBJS = raster3d_main.o +r_colors_out_OBJS = raster_main.o prt_json.o +r3_colors_out_OBJS = raster3d_main.o prt_json.o include $(MODULE_TOPDIR)/include/Make/Multi.make diff --git a/raster/r.colors.out/local_proto.h b/raster/r.colors.out/local_proto.h new file mode 100644 index 00000000000..5ed09a06e69 --- /dev/null +++ b/raster/r.colors.out/local_proto.h @@ -0,0 +1,7 @@ +#include +#include + +enum ColorFormat { RGB, HEX, HSV, TRIPLET }; + +void print_json_colors(struct Colors *colors, DCELL min, DCELL max, FILE *fp, + int perc, enum ColorFormat clr_frmt); diff --git a/raster/r.colors.out/prt_json.c b/raster/r.colors.out/prt_json.c new file mode 100644 index 00000000000..816a63e1b0e --- /dev/null +++ b/raster/r.colors.out/prt_json.c @@ -0,0 +1,265 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "local_proto.h" + +#define COLOR_STRING_LENGTH 30 + +/*! + \brief Closes the file if it is not stdout. + + \param fp file where to print color table rules + */ +static void close_file(FILE *fp) +{ + if (fp != stdout) + fclose(fp); +} + +/*! + \brief Converts RGB color values to HSV format. + + \note This implementation is experimental and may be subject to change. + + \param r red component of the RGB color + \param g green component of the RGB color + \param b blue component of the RGB color + \param[out] h pointer to store the calculated hue + \param[out] s pointer to store the calculated saturation + \param[out] v pointer to store the calculated value + */ +static void rgb_to_hsv(int r, int g, int b, float *h, float *s, float *v) +{ + float r_norm = (float)r / 255.0f; + float g_norm = (float)g / 255.0f; + float b_norm = (float)b / 255.0f; + + float cmax = MAX(r_norm, MAX(g_norm, b_norm)); + float cmin = MIN(r_norm, MIN(g_norm, b_norm)); + float diff = cmax - cmin; + + if (cmax == cmin) { + *h = 0; + } + else if (cmax == r_norm) { + *h = fmodf((60.0f * ((g_norm - b_norm) / diff) + 360.0f), 360.0f); + } + else if (cmax == g_norm) { + *h = fmodf((60.0f * ((b_norm - r_norm) / diff) + 120.0f), 360.0f); + } + else { + *h = fmodf((60.0f * ((r_norm - g_norm) / diff) + 240.0f), 360.0f); + } + + if (cmax == 0) { + *s = 0; + } + else { + *s = (diff / cmax) * 100.0f; + } + + *v = cmax * 100.0f; +} + +/*! + \brief Writes color entry in JSON in specified clr_frmt. + + \param r red component of RGB color + \param g green component of RGB color + \param b blue component of RGB color + \param clr_frmt color format to be used (RGB, HEX, HSV, TRIPLET). + \param color_object pointer to the JSON object + */ +static void set_color(int r, int g, int b, enum ColorFormat clr_frmt, + JSON_Object *color_object) +{ + char color_string[COLOR_STRING_LENGTH]; + float h, s, v; + + switch (clr_frmt) { + case RGB: + snprintf(color_string, sizeof(color_string), "rgb(%d, %d, %d)", r, g, + b); + json_object_set_string(color_object, "rgb", color_string); + break; + + case HEX: + snprintf(color_string, sizeof(color_string), "#%02X%02X%02X", r, g, b); + json_object_set_string(color_object, "hex", color_string); + break; + + case HSV: + rgb_to_hsv(r, g, b, &h, &s, &v); + snprintf(color_string, sizeof(color_string), "hsv(%d, %d, %d)", (int)h, + (int)s, (int)v); + json_object_set_string(color_object, "hsv", color_string); + break; + + case TRIPLET: + snprintf(color_string, sizeof(color_string), "%d:%d:%d", r, g, b); + json_object_set_string(color_object, "triplet", color_string); + break; + } +} + +/*! + \brief Writes a JSON rule for a specific color entry. + + \param val pointer to the DCELL value + \param min,max minimum and maximum value for percentage output (used only + when \p perc is non-zero) + \param r red component of RGB color + \param g green component of RGB color + \param b blue component of RGB color + \param root_array pointer to the JSON array + \param perc TRUE for percentage output + \param clr_frmt color format to be used (RBG, HEX, HSV, TRIPLET). + \param fp file where to print color table rules + \param root_value pointer to json value + */ +static void write_json_rule(DCELL *val, DCELL *min, DCELL *max, int r, int g, + int b, JSON_Array *root_array, int perc, + enum ColorFormat clr_frmt, FILE *fp, + JSON_Value *root_value) +{ + static DCELL v0; + static int r0 = -1, g0 = -1, b0 = -1; + + // Skip writing if the current color is the same as the last one + if (v0 == *val && r0 == r && g0 == g && b0 == b) + return; + // Update last processed values + v0 = *val, r0 = r, g0 = g, b0 = b; + + JSON_Value *color_value = json_value_init_object(); + if (color_value == NULL) { + json_value_free(root_value); + close_file(fp); + G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); + } + JSON_Object *color_object = json_object(color_value); + + // Set the value as a percentage if requested, otherwise set it as-is + if (perc) + json_object_set_number(color_object, "value", + 100 * (*val - *min) / (*max - *min)); + else + json_object_set_number(color_object, "value", *val); + + set_color(r, g, b, clr_frmt, color_object); + + json_array_append_value(root_array, color_value); +} + +/*! + \brief Print color table in JSON format + + \param colors pointer to Colors structure + \param min,max minimum and maximum value for percentage output (used only + when \p perc is non-zero) + \param fp file where to print color table rules + \param perc TRUE for percentage output + \param clr_frmt color format to be used (RBG, HEX, HSV, TRIPLET). + */ +void print_json_colors(struct Colors *colors, DCELL min, DCELL max, FILE *fp, + int perc, enum ColorFormat clr_frmt) +{ + JSON_Value *root_value = json_value_init_array(); + if (root_value == NULL) { + close_file(fp); + G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); + } + JSON_Array *root_array = json_array(root_value); + + if (colors->version < 0) { + /* 3.0 format */ + CELL lo, hi; + + // Retrieve the integer color range + Rast_get_c_color_range(&lo, &hi, colors); + + for (int i = lo; i <= hi; i++) { + unsigned char r, g, b, set; + DCELL val = (DCELL)i; + + // Look up the color for the current value and write JSON rule + Rast_lookup_c_colors(&i, &r, &g, &b, &set, 1, colors); + write_json_rule(&val, &min, &max, r, g, b, root_array, perc, + clr_frmt, fp, root_value); + } + } + else { + // Get the count of floating-point color rules + int count = Rast_colors_count(colors); + + for (int i = 0; i < count; i++) { + DCELL val1, val2; + unsigned char r1, g1, b1, r2, g2, b2; + + // Retrieve the color rule values and their respective RGB colors + Rast_get_fp_color_rule(&val1, &r1, &g1, &b1, &val2, &r2, &g2, &b2, + colors, count - 1 - i); + + // write JSON rule + write_json_rule(&val1, &min, &max, r1, g1, b1, root_array, perc, + clr_frmt, fp, root_value); + write_json_rule(&val2, &min, &max, r2, g2, b2, root_array, perc, + clr_frmt, fp, root_value); + } + } + + // Add special color entries for "null" and "default" values + { + int r, g, b; + + // Get RGB color for null values and create JSON entry + Rast_get_null_value_color(&r, &g, &b, colors); + JSON_Value *nv_value = json_value_init_object(); + if (nv_value == NULL) { + json_value_free(root_value); + close_file(fp); + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + JSON_Object *nv_object = json_object(nv_value); + json_object_set_string(nv_object, "value", "nv"); + set_color(r, g, b, clr_frmt, nv_object); + json_array_append_value(root_array, nv_value); + + // Get RGB color for default values and create JSON entry + Rast_get_default_color(&r, &g, &b, colors); + JSON_Value *default_value = json_value_init_object(); + if (default_value == NULL) { + json_value_free(root_value); + close_file(fp); + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + JSON_Object *default_object = json_object(default_value); + json_object_set_string(default_object, "value", "default"); + set_color(r, g, b, clr_frmt, default_object); + json_array_append_value(root_array, default_value); + } + + // Serialize JSON array to a string and print to the file + char *json_string = json_serialize_to_string_pretty(root_value); + if (!json_string) { + json_value_free(root_value); + close_file(fp); + G_fatal_error(_("Failed to serialize JSON to pretty format.")); + } + + fputs(json_string, fp); + + json_free_serialized_string(json_string); + json_value_free(root_value); + + close_file(fp); +} diff --git a/raster/r.colors.out/r.colors.out.html b/raster/r.colors.out/r.colors.out.html index 29423001db6..357bd1ebf6c 100644 --- a/raster/r.colors.out/r.colors.out.html +++ b/raster/r.colors.out/r.colors.out.html @@ -9,6 +9,7 @@

EXAMPLES

 r.colors.out map=el_D782_6m rules=rules.txt
 r.colors map=el_D783_6m rules=rules.txt
+r.colors.out map=el_D782_6m rules=rules.json format=json
 

SEE ALSO

diff --git a/raster/r.colors.out/r3.colors.out.html b/raster/r.colors.out/r3.colors.out.html index f86bf57b89c..04472d4de8c 100644 --- a/raster/r.colors.out/r3.colors.out.html +++ b/raster/r.colors.out/r3.colors.out.html @@ -9,6 +9,7 @@

EXAMPLES

 r3.colors.out map=volume_1 rules=rules.txt
 r3.colors map=volume_2 rules=rules.txt
+r3.colors.out map=volume_1 rules=rules.json format=json
 

SEE ALSO

diff --git a/raster/r.colors.out/raster3d_main.c b/raster/r.colors.out/raster3d_main.c index 95c05e545f1..904db24a91f 100644 --- a/raster/r.colors.out/raster3d_main.c +++ b/raster/r.colors.out/raster3d_main.c @@ -21,13 +21,16 @@ #include #include #include +#include + +#include "local_proto.h" /* Run in raster3d mode */ int main(int argc, char **argv) { struct GModule *module; struct { - struct Option *map, *file; + struct Option *map, *file, *format, *color_format; } opt; struct { struct Flag *p; @@ -38,6 +41,8 @@ int main(int argc, char **argv) struct Colors colors; struct FPRange range; + enum ColorFormat clr_frmt; + G_gisinit(argv[0]); module = G_define_module(); @@ -55,6 +60,12 @@ int main(int argc, char **argv) opt.file->description = _("If not given write to standard output"); opt.file->required = NO; + opt.format = G_define_standard_option(G_OPT_F_FORMAT); + opt.format->guisection = _("Print"); + + opt.color_format = G_define_standard_option(G_OPT_C_FORMAT); + opt.color_format->guisection = _("Color"); + flag.p = G_define_flag(); flag.p->key = 'p'; flag.p->description = _("Output values as percentages"); @@ -78,8 +89,26 @@ int main(int argc, char **argv) G_fatal_error(_("Unable to open output file <%s>"), file); } - Rast_print_colors(&colors, range.min, range.max, fp, - flag.p->answer ? 1 : 0); + if (strcmp(opt.format->answer, "json") == 0) { + if (strcmp(opt.color_format->answer, "rgb") == 0) { + clr_frmt = RGB; + } + else if (strcmp(opt.color_format->answer, "triplet") == 0) { + clr_frmt = TRIPLET; + } + else if (strcmp(opt.color_format->answer, "hsv") == 0) { + clr_frmt = HSV; + } + else { + clr_frmt = HEX; + } + print_json_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0, clr_frmt); + } + else { + Rast_print_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0); + } exit(EXIT_SUCCESS); } diff --git a/raster/r.colors.out/raster_main.c b/raster/r.colors.out/raster_main.c index b2a0a139157..6b4a8ed36ee 100644 --- a/raster/r.colors.out/raster_main.c +++ b/raster/r.colors.out/raster_main.c @@ -20,13 +20,16 @@ #include #include #include +#include + +#include "local_proto.h" /* Run in raster mode */ int main(int argc, char **argv) { struct GModule *module; struct { - struct Option *map, *file; + struct Option *map, *file, *format, *color_format; } opt; struct { struct Flag *p; @@ -37,6 +40,8 @@ int main(int argc, char **argv) struct Colors colors; struct FPRange range; + enum ColorFormat clr_frmt; + G_gisinit(argv[0]); module = G_define_module(); @@ -54,6 +59,12 @@ int main(int argc, char **argv) opt.file->description = _("If not given write to standard output"); opt.file->required = NO; + opt.format = G_define_standard_option(G_OPT_F_FORMAT); + opt.format->guisection = _("Print"); + + opt.color_format = G_define_standard_option(G_OPT_C_FORMAT); + opt.color_format->guisection = _("Color"); + flag.p = G_define_flag(); flag.p->key = 'p'; flag.p->description = _("Output values as percentages"); @@ -77,8 +88,26 @@ int main(int argc, char **argv) G_fatal_error(_("Unable to open output file <%s>"), file); } - Rast_print_colors(&colors, range.min, range.max, fp, - flag.p->answer ? 1 : 0); + if (strcmp(opt.format->answer, "json") == 0) { + if (strcmp(opt.color_format->answer, "rgb") == 0) { + clr_frmt = RGB; + } + else if (strcmp(opt.color_format->answer, "triplet") == 0) { + clr_frmt = TRIPLET; + } + else if (strcmp(opt.color_format->answer, "hsv") == 0) { + clr_frmt = HSV; + } + else { + clr_frmt = HEX; + } + print_json_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0, clr_frmt); + } + else { + Rast_print_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0); + } exit(EXIT_SUCCESS); } diff --git a/raster/r.colors.out/tests/conftest.py b/raster/r.colors.out/tests/conftest.py new file mode 100644 index 00000000000..c0c6790a75c --- /dev/null +++ b/raster/r.colors.out/tests/conftest.py @@ -0,0 +1,52 @@ +"""Fixture for r.colors.out and r3.colors.out test""" + +import os +import pytest +import grass.script as gs + + +@pytest.fixture +def raster_color_dataset(tmp_path): + """Set up a GRASS session and create test rasters with color rules.""" + project = tmp_path / "raster_color_project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + gs.run_command( + "g.region", + s=0, + n=90, + w=0, + e=100, + b=0, + t=1, + rows=3, + cols=3, + res=10, + env=session.env, + ) + gs.mapcalc("a = int(row())", env=session.env) + gs.run_command("r.colors", map="a", color="elevation", env=session.env) + yield session + + +@pytest.fixture +def raster3_color_dataset(tmp_path): + """Set up a GRASS session and create test raster3 with color rules.""" + project = tmp_path / "raster3_color_project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + gs.run_command( + "g.region", + s=0, + n=100, + w=0, + e=100, + b=5, + t=50, + tbres=10, + res3=20, + env=session.env, + ) + gs.mapcalc3d("b = double(row())", env=session.env) + gs.run_command("r3.colors", map="b", color="elevation", env=session.env) + yield session diff --git a/raster/r.colors.out/tests/r3_colors_out_test.py b/raster/r.colors.out/tests/r3_colors_out_test.py new file mode 100644 index 00000000000..670c1ef13a6 --- /dev/null +++ b/raster/r.colors.out/tests/r3_colors_out_test.py @@ -0,0 +1,175 @@ +"""Tests of r3.colors.out""" + +import grass.script as gs + + +def validate_plain_text_output(data): + """Validate the structure and content of the plain text output.""" + assert data + assert len(data) == 8, "The output does not match the expected number of items (8)." + expected = { + "1 0:191:191", + "1.8 0:255:0", + "2.6 255:255:0", + "3.4 255:127:0", + "4.2 191:127:63", + "5 200:200:200", + "nv 255:255:255", + "default 255:255:255", + } + assert ( + expected == data.keys() + ), f"test failed: expected {expected} but got {data.keys()}" + + +def test_r3_colors_out_plain_output(raster3_color_dataset): + """Test r3.colors.out command for plain output format.""" + session = raster3_color_dataset + data = gs.parse_command("r3.colors.out", map="b", format="plain", env=session.env) + validate_plain_text_output(data) + + +def test_r3_colors_out_without_format_option(raster3_color_dataset): + """Test r3.colors.out command without any format option.""" + session = raster3_color_dataset + data = gs.parse_command("r3.colors.out", map="b", env=session.env) + validate_plain_text_output(data) + + +def test_r3_colors_out_with_p_flag(raster3_color_dataset): + """Test r3.colors.out command with percentage values.""" + session = raster3_color_dataset + data = gs.parse_command("r3.colors.out", map="b", flags="p", env=session.env) + assert data + assert len(data) == 8, "The output does not match the expected number of items (8)." + expected = { + "0% 0:191:191", + "20% 0:255:0", + "40% 255:255:0", + "60% 255:127:0", + "80% 191:127:63", + "100% 200:200:200", + "nv 255:255:255", + "default 255:255:255", + } + assert ( + expected == data.keys() + ), f"test failed: expected {expected} but got {data.keys()}" + + +def validate_common_json_structure(data): + """Validate the common structure and content of the JSON output.""" + assert isinstance(data, list), "Output data should be a list of entries." + assert ( + len(data) == 8 + ), "The length of the output JSON does not match the expected value of 8." + + +def test_r3_colors_out_json_with_default_option(raster3_color_dataset): + """Test r3.colors.out command for JSON output format for default color option.""" + session = raster3_color_dataset + data = gs.parse_command("r3.colors.out", map="b", format="json", env=session.env) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hex": "#00BFBF"}, + {"value": 1.8, "hex": "#00FF00"}, + {"value": 2.6, "hex": "#FFFF00"}, + {"value": 3.4, "hex": "#FF7F00"}, + {"value": 4.2, "hex": "#BF7F3F"}, + {"value": 5, "hex": "#C8C8C8"}, + {"value": "nv", "hex": "#FFFFFF"}, + {"value": "default", "hex": "#FFFFFF"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r3_colors_out_json_with_triplet_option(raster3_color_dataset): + """Test r3.colors.out command for JSON output format for triplet color option.""" + session = raster3_color_dataset + data = gs.parse_command( + "r3.colors.out", map="b", format="json", color_format="triplet", env=session.env + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "triplet": "0:191:191"}, + {"value": 1.8, "triplet": "0:255:0"}, + {"value": 2.6, "triplet": "255:255:0"}, + {"value": 3.4, "triplet": "255:127:0"}, + {"value": 4.2, "triplet": "191:127:63"}, + {"value": 5, "triplet": "200:200:200"}, + {"value": "nv", "triplet": "255:255:255"}, + {"value": "default", "triplet": "255:255:255"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r3_colors_out_json_with_rgb_option(raster3_color_dataset): + """Test r3.colors.out command for JSON output format for rgb color option.""" + session = raster3_color_dataset + data = gs.parse_command( + "r3.colors.out", + map="b", + format="json", + color_format="rgb", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "rgb": "rgb(0, 191, 191)"}, + {"value": 1.8, "rgb": "rgb(0, 255, 0)"}, + {"value": 2.6, "rgb": "rgb(255, 255, 0)"}, + {"value": 3.4, "rgb": "rgb(255, 127, 0)"}, + {"value": 4.2, "rgb": "rgb(191, 127, 63)"}, + {"value": 5, "rgb": "rgb(200, 200, 200)"}, + {"value": "nv", "rgb": "rgb(255, 255, 255)"}, + {"value": "default", "rgb": "rgb(255, 255, 255)"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r3_colors_out_json_with_hex_option(raster3_color_dataset): + """Test r3.colors.out command for JSON output format for hex color option.""" + session = raster3_color_dataset + data = gs.parse_command( + "r3.colors.out", + map="b", + format="json", + color_format="hex", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hex": "#00BFBF"}, + {"value": 1.8, "hex": "#00FF00"}, + {"value": 2.6, "hex": "#FFFF00"}, + {"value": 3.4, "hex": "#FF7F00"}, + {"value": 4.2, "hex": "#BF7F3F"}, + {"value": 5, "hex": "#C8C8C8"}, + {"value": "nv", "hex": "#FFFFFF"}, + {"value": "default", "hex": "#FFFFFF"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r3_colors_out_json_with_hsv_option(raster3_color_dataset): + """Test r3.colors.out command for JSON output format for hsv color option.""" + session = raster3_color_dataset + data = gs.parse_command( + "r3.colors.out", + map="b", + format="json", + color_format="hsv", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hsv": "hsv(180, 100, 74)"}, + {"value": 1.8, "hsv": "hsv(120, 100, 100)"}, + {"value": 2.6, "hsv": "hsv(60, 100, 100)"}, + {"value": 3.4, "hsv": "hsv(29, 100, 100)"}, + {"value": 4.2, "hsv": "hsv(30, 67, 74)"}, + {"value": 5, "hsv": "hsv(0, 0, 78)"}, + {"value": "nv", "hsv": "hsv(0, 0, 100)"}, + {"value": "default", "hsv": "hsv(0, 0, 100)"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" diff --git a/raster/r.colors.out/tests/r_colors_out_test.py b/raster/r.colors.out/tests/r_colors_out_test.py new file mode 100644 index 00000000000..d1abec82592 --- /dev/null +++ b/raster/r.colors.out/tests/r_colors_out_test.py @@ -0,0 +1,175 @@ +"""Tests of r.colors.out""" + +import grass.script as gs + + +def validate_plain_text_output(data): + """Validate the structure and content of the plain text output.""" + assert data + assert len(data) == 8, "The output does not match the expected number of items (8)." + expected = { + "1 0:191:191", + "1.4 0:255:0", + "1.8 255:255:0", + "2.2 255:127:0", + "2.6 191:127:63", + "3 200:200:200", + "nv 255:255:255", + "default 255:255:255", + } + assert ( + expected == data.keys() + ), f"test failed: expected {expected} but got {data.keys()}" + + +def test_r_colors_out_plain_output(raster_color_dataset): + """Test r.colors.out command for plain output format.""" + session = raster_color_dataset + data = gs.parse_command("r.colors.out", map="a", format="plain", env=session.env) + validate_plain_text_output(data) + + +def test_r_colors_out_without_format_option(raster_color_dataset): + """Test r.colors.out command without any format option.""" + session = raster_color_dataset + data = gs.parse_command("r.colors.out", map="a", env=session.env) + validate_plain_text_output(data) + + +def test_r_colors_out_with_p_flag(raster_color_dataset): + """Test r.colors.out command with percentage values.""" + session = raster_color_dataset + data = gs.parse_command("r.colors.out", map="a", flags="p", env=session.env) + assert data + assert len(data) == 8, "The output does not match the expected number of items (8)." + expected = { + "0% 0:191:191", + "20% 0:255:0", + "40% 255:255:0", + "60% 255:127:0", + "80% 191:127:63", + "100% 200:200:200", + "nv 255:255:255", + "default 255:255:255", + } + assert ( + expected == data.keys() + ), f"test failed: expected {expected} but got {data.keys()}" + + +def validate_common_json_structure(data): + """Validate the common structure and content of the JSON output.""" + assert isinstance(data, list), "Output data should be a list of entries." + assert ( + len(data) == 8 + ), "The length of the output JSON does not match the expected value of 8." + + +def test_r_colors_out_json_with_default_option(raster_color_dataset): + """Test r.colors.out command for JSON output format for default color option.""" + session = raster_color_dataset + data = gs.parse_command("r.colors.out", map="a", format="json", env=session.env) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hex": "#00BFBF"}, + {"value": 1.4, "hex": "#00FF00"}, + {"value": 1.8, "hex": "#FFFF00"}, + {"value": 2.2, "hex": "#FF7F00"}, + {"value": 2.6, "hex": "#BF7F3F"}, + {"value": 3, "hex": "#C8C8C8"}, + {"value": "nv", "hex": "#FFFFFF"}, + {"value": "default", "hex": "#FFFFFF"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r_colors_out_json_with_triplet_option(raster_color_dataset): + """Test r.colors.out command for JSON output format for triplet color option.""" + session = raster_color_dataset + data = gs.parse_command( + "r.colors.out", map="a", format="json", color_format="triplet", env=session.env + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "triplet": "0:191:191"}, + {"value": 1.4, "triplet": "0:255:0"}, + {"value": 1.8, "triplet": "255:255:0"}, + {"value": 2.2, "triplet": "255:127:0"}, + {"value": 2.6, "triplet": "191:127:63"}, + {"value": 3, "triplet": "200:200:200"}, + {"value": "nv", "triplet": "255:255:255"}, + {"value": "default", "triplet": "255:255:255"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r_colors_out_json_with_rgb_option(raster_color_dataset): + """Test r.colors.out command for JSON output format for rgb color option.""" + session = raster_color_dataset + data = gs.parse_command( + "r.colors.out", + map="a", + format="json", + color_format="rgb", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "rgb": "rgb(0, 191, 191)"}, + {"value": 1.4, "rgb": "rgb(0, 255, 0)"}, + {"value": 1.8, "rgb": "rgb(255, 255, 0)"}, + {"value": 2.2, "rgb": "rgb(255, 127, 0)"}, + {"value": 2.6, "rgb": "rgb(191, 127, 63)"}, + {"value": 3, "rgb": "rgb(200, 200, 200)"}, + {"value": "nv", "rgb": "rgb(255, 255, 255)"}, + {"value": "default", "rgb": "rgb(255, 255, 255)"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r_colors_out_json_with_hex_option(raster_color_dataset): + """Test r.colors.out command for JSON output format for hex color option.""" + session = raster_color_dataset + data = gs.parse_command( + "r.colors.out", + map="a", + format="json", + color_format="hex", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hex": "#00BFBF"}, + {"value": 1.4, "hex": "#00FF00"}, + {"value": 1.8, "hex": "#FFFF00"}, + {"value": 2.2, "hex": "#FF7F00"}, + {"value": 2.6, "hex": "#BF7F3F"}, + {"value": 3, "hex": "#C8C8C8"}, + {"value": "nv", "hex": "#FFFFFF"}, + {"value": "default", "hex": "#FFFFFF"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}" + + +def test_r_colors_out_json_with_hsv_option(raster_color_dataset): + """Test r.colors.out command for JSON output format for hsv color option.""" + session = raster_color_dataset + data = gs.parse_command( + "r.colors.out", + map="a", + format="json", + color_format="hsv", + env=session.env, + ) + validate_common_json_structure(data) + expected = [ + {"value": 1, "hsv": "hsv(180, 100, 74)"}, + {"value": 1.4, "hsv": "hsv(120, 100, 100)"}, + {"value": 1.8, "hsv": "hsv(60, 100, 100)"}, + {"value": 2.2, "hsv": "hsv(29, 100, 100)"}, + {"value": 2.6, "hsv": "hsv(30, 67, 74)"}, + {"value": 3, "hsv": "hsv(0, 0, 78)"}, + {"value": "nv", "hsv": "hsv(0, 0, 100)"}, + {"value": "default", "hsv": "hsv(0, 0, 100)"}, + ] + assert expected == data, f"test failed: expected {expected} but got {data}"