From dfe6501be401b5067fa5009788ed5efe30a99f5f Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sun, 20 Oct 2024 17:15:06 +0530 Subject: [PATCH] r.colors.out: added json output Signed-off-by: Nishant Bansal --- raster/r.colors.out/Makefile | 8 +- raster/r.colors.out/local_proto.h | 10 ++ raster/r.colors.out/prt_json.c | 107 ++++++++++++++++++ raster/r.colors.out/raster3d_main.c | 27 ++++- raster/r.colors.out/raster_main.c | 27 ++++- raster/r.colors.out/tests/conftest.py | 83 ++++++++++++++ .../r.colors.out/tests/r3_colors_out_test.py | 23 ++++ .../r.colors.out/tests/r_colors_out_test.py | 23 ++++ 8 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 raster/r.colors.out/local_proto.h create mode 100644 raster/r.colors.out/prt_json.c create mode 100644 raster/r.colors.out/tests/conftest.py create mode 100644 raster/r.colors.out/tests/r3_colors_out_test.py create mode 100644 raster/r.colors.out/tests/r_colors_out_test.py 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..211af04355a --- /dev/null +++ b/raster/r.colors.out/local_proto.h @@ -0,0 +1,10 @@ +#include +#include + +enum OutputFormat { PLAIN, JSON }; + +void write_json_rule(DCELL *val, DCELL *min, DCELL *max, int r, int g, int b, + FILE *fp, JSON_Array *root_array, int perc); + +void Rast_json_print_colors(struct Colors *colors, DCELL min, DCELL max, + FILE *fp, int perc); \ No newline at end of file diff --git a/raster/r.colors.out/prt_json.c b/raster/r.colors.out/prt_json.c new file mode 100644 index 00000000000..5470a772f7d --- /dev/null +++ b/raster/r.colors.out/prt_json.c @@ -0,0 +1,107 @@ +#include +#include +#include +#include + +void write_json_rule(DCELL *val, DCELL *min, DCELL *max, int r, int g, int b, + FILE *fp, JSON_Array *root_array, int perc) +{ + static DCELL v0; + static int r0 = -1, g0 = -1, b0 = -1; + + if (v0 == *val && r0 == r && g0 == g && b0 == b) + return; + v0 = *val, r0 = r, g0 = g, b0 = b; + JSON_Value *color_value = json_value_init_object(); + JSON_Object *color_object = json_object(color_value); + + char rgb_string[20]; + snprintf(rgb_string, sizeof(rgb_string), "rgb(%d, %d, %d)", r, g, b); + + if (perc) + json_object_set_number(color_object, "value", + 100 * (*val - *min) / (*max - *min)); + else + json_object_set_number(color_object, "value", *val); + + json_object_set_string(color_object, "rgb", rgb_string); + + json_array_append_value(root_array, color_value); +} + +void Rast_json_print_colors(struct Colors *colors, DCELL min, DCELL max, + FILE *fp, int perc) +{ + JSON_Value *root_value = json_value_init_array(); + JSON_Array *root_array = json_array(root_value); + + int i, count; + + count = 0; + if (colors->version < 0) { + CELL lo, hi; + + Rast_get_c_color_range(&lo, &hi, colors); + + for (i = lo; i <= hi; i++) { + unsigned char r, g, b, set; + DCELL val = (DCELL)i; + + Rast_lookup_c_colors(&i, &r, &g, &b, &set, 1, colors); + write_json_rule(&val, &min, &max, r, g, b, fp, root_array, perc); + } + } + else { + count = Rast_colors_count(colors); + + for (i = 0; i < count; i++) { + DCELL val1, val2; + unsigned char r1, g1, b1, r2, g2, b2; + + Rast_get_fp_color_rule(&val1, &r1, &g1, &b1, &val2, &r2, &g2, &b2, + colors, count - 1 - i); + + write_json_rule(&val1, &min, &max, r1, g1, b1, fp, root_array, + perc); + write_json_rule(&val2, &min, &max, r2, g2, b2, fp, root_array, + perc); + } + } + + { + int r, g, b; + + Rast_get_null_value_color(&r, &g, &b, colors); + JSON_Value *nv_value = json_value_init_object(); + JSON_Object *nv_object = json_object(nv_value); + char nv_rgb_string[20]; + snprintf(nv_rgb_string, sizeof(nv_rgb_string), "rgb(%d, %d, %d)", r, g, + b); + json_object_set_string(nv_object, "value", "nv"); + json_object_set_string(nv_object, "rgb", nv_rgb_string); + json_array_append_value(root_array, nv_value); + + Rast_get_default_color(&r, &g, &b, colors); + JSON_Value *default_value = json_value_init_object(); + JSON_Object *default_object = json_object(default_value); + char default_rgb_string[20]; + snprintf(default_rgb_string, sizeof(default_rgb_string), + "rgb(%d, %d, %d)", r, g, b); + json_object_set_string(default_object, "value", "default"); + json_object_set_string(default_object, "rgb", default_rgb_string); + json_array_append_value(root_array, default_value); + } + + char *json_string = json_serialize_to_string_pretty(root_value); + if (!json_string) { + 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); + + if (fp != stdout) + fclose(fp); +} \ No newline at end of file diff --git a/raster/r.colors.out/raster3d_main.c b/raster/r.colors.out/raster3d_main.c index 95c05e545f1..f7fef3475bb 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; } opt; struct { struct Flag *p; @@ -38,6 +41,8 @@ int main(int argc, char **argv) struct Colors colors; struct FPRange range; + enum OutputFormat format; + G_gisinit(argv[0]); module = G_define_module(); @@ -55,6 +60,9 @@ 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"); + flag.p = G_define_flag(); flag.p->key = 'p'; flag.p->description = _("Output values as percentages"); @@ -78,8 +86,21 @@ 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) { + format = JSON; + } + else { + format = PLAIN; + } + + if (format == JSON) { + Rast_json_print_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0); + } + 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..27073d6c7ac 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; } opt; struct { struct Flag *p; @@ -37,6 +40,8 @@ int main(int argc, char **argv) struct Colors colors; struct FPRange range; + enum OutputFormat format; + G_gisinit(argv[0]); module = G_define_module(); @@ -54,6 +59,9 @@ 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"); + flag.p = G_define_flag(); flag.p->key = 'p'; flag.p->description = _("Output values as percentages"); @@ -77,8 +85,21 @@ 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) { + format = JSON; + } + else { + format = PLAIN; + } + + if (format == JSON) { + Rast_json_print_colors(&colors, range.min, range.max, fp, + flag.p->answer ? 1 : 0); + } + 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..1ed2d84bccd --- /dev/null +++ b/raster/r.colors.out/tests/conftest.py @@ -0,0 +1,83 @@ +"""Fixture for r.colors.out and r3.colors.out test""" + +from types import SimpleNamespace + +import os +import pytest +import grass.script as gs + +def setup_grass_location(tmp_path, location_name): + """Initialize a new GRASS location.""" + gs.core._create_location_xy(tmp_path, location_name) + location_path = tmp_path / location_name + return location_path + +def configure_grass_region(session_env): + """Configure the GRASS region for testing with defined bounds and resolution.""" + gs.run_command( + "g.region", + s=0, n=90, w=0, e=100, b=0, t=50, res=10, res3=10, env=session_env + ) + +def create_test_rasters(session_env): + """Generate raster layers with specific values for testing.""" + gs.run_command("r.mapcalc", expression="test_elev_int_1 = int(rand(-15.0, 5.0))", seed=1, env=session_env) + gs.run_command("r.mapcalc", expression="test_elev_int_2 = int(rand(0.0, 10.0))", seed=1, env=session_env) + gs.run_command("r.mapcalc", expression="test_elev_int_3 = int(rand(5.0, 15.0))", seed=1, env=session_env) + + return "test_elev_int_1,test_elev_int_2,test_elev_int_3" + +def apply_random_color_to_rasters(raster_names, session_env): + """Apply random colors to the specified rasters.""" + gs.run_command("r.colors", map=raster_names, color="random", env=session_env) + +def create_test_rasters3(session_env): + """Generate raster3 layers with specific values for testing.""" + gs.run_command("r3.mapcalc", expression="volume_double = double(col() + row() + depth())", env=session_env) + gs.run_command("r3.mapcalc", expression="volume_double_null = if(row() == 1 || row() == 5, null(), volume_double)", env=session_env) + + return "volume_double_null" + +def apply_random_color_to_rasters3(raster3_names, session_env): + """Apply elevation colors to the specified raster3.""" + gs.run_command("r3.colors", map=raster3_names, color="elevation", env=session_env) + +@pytest.fixture +def raster_color_dataset(tmp_path_factory): + """Set up a GRASS session and create test rasters with color rules.""" + + tmp_path = tmp_path_factory.mktemp("raster_color_test") + location_name = "test_location" + location_path = setup_grass_location(tmp_path, location_name) + + with gs.setup.init(location_path, env=os.environ.copy()) as session: + configure_grass_region(session.env) + + raster_names = create_test_rasters(session.env) + apply_random_color_to_rasters(raster_names, session.env) + + yield SimpleNamespace( + session=session, + raster_names="test_elev_int_3", + env=session.env, + ) + +@pytest.fixture +def raster3_color_dataset(tmp_path_factory): + """Set up a GRASS session and create test raster3 with color rules.""" + + tmp_path = tmp_path_factory.mktemp("raster3_color_test") + location_name = "test_location" + location_path = setup_grass_location(tmp_path, location_name) + + with gs.setup.init(location_path, env=os.environ.copy()) as session: + configure_grass_region(session.env) + + raster3_names = create_test_rasters3(session.env) + apply_random_color_to_rasters3(raster3_names, session.env) + + yield SimpleNamespace( + session=session, + raster3_names=raster3_names, + env=session.env, + ) \ No newline at end of file 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..b3501ec93ba --- /dev/null +++ b/raster/r.colors.out/tests/r3_colors_out_test.py @@ -0,0 +1,23 @@ +"""Tests of r3.colors.out""" + +import pytest +import json +import grass.script as gs + +def validate_json_structure(data): + """Validate the 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." + + assert all("value" in entry for entry in data), "Not all entries contain the 'value' key." + assert all("rgb" in entry for entry in data), "Not all entries contain the 'rgb' key." + + assert any(entry.get("value") == "nv" for entry in data), "No entry contains 'nv' as the value." + assert any(entry.get("value") == "default" for entry in data), "No entry contains 'default' as the value." + +def test_r3_colors_out_json(raster3_color_dataset): + """Test r3.colors.out command for JSON output format.""" + session = raster3_color_dataset + data = gs.parse_command("r3.colors.out", map=session.raster3_names, format="json", env=session.env) + + validate_json_structure(data) \ No newline at end of file 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..816556c09b2 --- /dev/null +++ b/raster/r.colors.out/tests/r_colors_out_test.py @@ -0,0 +1,23 @@ +"""Tests of r.colors.out""" + +import pytest +import json +import grass.script as gs + +def validate_json_structure(data): + """Validate the structure and content of the JSON output.""" + assert isinstance(data, list), "Output data should be a list of entries." + assert len(data) == 31, "The length of the output JSON does not match the expected value of 31." + + assert all("value" in entry for entry in data), "Not all entries contain the 'value' key." + assert all("rgb" in entry for entry in data), "Not all entries contain the 'rgb' key." + + assert any(entry.get("value") == "nv" for entry in data), "No entry contains 'nv' as the value." + assert any(entry.get("value") == "default" for entry in data), "No entry contains 'default' as the value." + +def test_r_colors_out_json(raster_color_dataset): + """Test r.colors.out command for JSON output format.""" + session = raster_color_dataset + data = gs.parse_command("r.colors.out", map=session.raster_names, format="json", env=session.env) + + validate_json_structure(data)