From c8293c0f43cb05be440fd17962370c6bf3ac6e74 Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Tue, 28 May 2024 01:05:12 +0530 Subject: [PATCH 1/9] Add JSON support to r.info module Use parson to add json output format support to the r.info module. The module has various flags to control the fields being output in case of plain format. The current prototype adheres to the flags for JSON output as well. --- raster/r.info/Makefile | 2 +- raster/r.info/main.c | 412 +++++++++++++++++++------ raster/r.info/testsuite/test_r_info.py | 79 +++++ 3 files changed, 392 insertions(+), 101 deletions(-) diff --git a/raster/r.info/Makefile b/raster/r.info/Makefile index 3cfdfce125a..a04fa7e4749 100644 --- a/raster/r.info/Makefile +++ b/raster/r.info/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = r.info -LIBES = $(RASTERLIB) $(GISLIB) +LIBES = $(RASTERLIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(RASTERDEP) $(GISDEP) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/raster/r.info/main.c b/raster/r.info/main.c index b70135225a7..a30f5c42573 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "local_proto.h" #define printline(x) fprintf(out, " | %-74.74s |\n", x) @@ -34,6 +35,8 @@ fprintf(out, "-"); \ fprintf(out, "%c\n", x) +enum OutputFormat { PLAIN, JSON }; + /* local prototypes */ static void format_double(const double, char *); static void compose_line(FILE *, const char *, ...); @@ -42,7 +45,7 @@ int main(int argc, char **argv) { const char *name, *mapset; const char *title; - char tmp1[100], tmp2[100], tmp3[100]; + char tmp1[100], tmp2[100], tmp3[100], tmp4[100]; char timebuff[256]; char *units, *vdatum, *semantic_label; int i; @@ -61,8 +64,12 @@ int main(int argc, char **argv) RASTER_MAP_TYPE data_type; struct Reclass reclass; struct GModule *module; - struct Option *opt1; + struct Option *opt1, *fopt; struct Flag *gflag, *rflag, *eflag, *hflag, *sflag; + enum OutputFormat format; + + JSON_Value *root_value; + JSON_Object *root_object; /* Initialize GIS Engine */ G_gisinit(argv[0]); @@ -98,6 +105,9 @@ int main(int argc, char **argv) hflag->key = 'h'; hflag->description = _("Print raster history instead of info"); + fopt = G_define_standard_option(G_OPT_F_FORMAT); + fopt->guisection = _("Print"); + if (G_parser(argc, argv)) exit(EXIT_FAILURE); @@ -110,6 +120,17 @@ int main(int argc, char **argv) if ((mapset = G_find_raster2(name, "")) == NULL) G_fatal_error(_("Raster map <%s> not found"), name); + if (strcmp(fopt->answer, "json") == 0) + format = JSON; + else + format = PLAIN; + + if (format == JSON) { + root_value = json_value_init_object(); + root_object = json_value_get_object(root_value); + json_set_float_serialization_format("%lf"); + } + Rast_get_cellhd(name, "", &cellhd); cats_ok = Rast_read_cats(name, "", &cats) >= 0; title = Rast_get_cats_title(&cats); @@ -152,7 +173,7 @@ int main(int argc, char **argv) } if (!gflag->answer && !rflag->answer && !sflag->answer && !eflag->answer && - !hflag->answer) { + !hflag->answer && format == PLAIN) { divider('+'); compose_line(out, "Map: %-29.29s Date: %s", name, @@ -441,37 +462,63 @@ int main(int argc, char **argv) } if (gflag->answer) { - G_format_northing(cellhd.north, tmp1, -1); - G_format_northing(cellhd.south, tmp2, -1); - fprintf(out, "north=%s\n", tmp1); - fprintf(out, "south=%s\n", tmp2); - - G_format_easting(cellhd.east, tmp1, -1); - G_format_easting(cellhd.west, tmp2, -1); - fprintf(out, "east=%s\n", tmp1); - fprintf(out, "west=%s\n", tmp2); - - G_format_resolution(cellhd.ns_res, tmp3, -1); - fprintf(out, "nsres=%s\n", tmp3); - - G_format_resolution(cellhd.ew_res, tmp3, -1); - fprintf(out, "ewres=%s\n", tmp3); - - fprintf(out, "rows=%d\n", cellhd.rows); - fprintf(out, "cols=%d\n", cellhd.cols); - - fprintf(out, "cells=%" PRId64 "\n", - (grass_int64)cellhd.rows * cellhd.cols); - - fprintf(out, "datatype=%s\n", - (data_type == CELL_TYPE - ? "CELL" - : (data_type == DCELL_TYPE - ? "DCELL" - : (data_type == FCELL_TYPE ? "FCELL" : "??")))); - if (cats_ok) - format_double((double)cats.num, tmp1); - fprintf(out, "ncats=%s\n", cats_ok ? tmp1 : "??"); + const char* data_type_f = (data_type == CELL_TYPE + ? "CELL" + : (data_type == DCELL_TYPE + ? "DCELL" + : (data_type == FCELL_TYPE ? "FCELL" : "??"))); + grass_int64 total_cells = (grass_int64)cellhd.rows * cellhd.cols; + + switch (format) { + case PLAIN: + G_format_northing(cellhd.north, tmp1, -1); + G_format_northing(cellhd.south, tmp2, -1); + fprintf(out, "north=%s\n", tmp1); + fprintf(out, "south=%s\n", tmp2); + + G_format_easting(cellhd.east, tmp1, -1); + G_format_easting(cellhd.west, tmp2, -1); + fprintf(out, "east=%s\n", tmp1); + fprintf(out, "west=%s\n", tmp2); + + G_format_resolution(cellhd.ns_res, tmp3, -1); + fprintf(out, "nsres=%s\n", tmp3); + + G_format_resolution(cellhd.ew_res, tmp3, -1); + fprintf(out, "ewres=%s\n", tmp3); + + fprintf(out, "rows=%d\n", cellhd.rows); + fprintf(out, "cols=%d\n", cellhd.cols); + + fprintf(out, "cells=%" PRId64 "\n", total_cells); + + fprintf(out, "datatype=%s\n", data_type_f); + + if (cats_ok) + format_double((double)cats.num, tmp4); + fprintf(out, "ncats=%s\n", cats_ok ? tmp4 : "??"); + break; + case JSON: + json_object_set_number(root_object, "north", cellhd.north); + json_object_set_number(root_object, "south", cellhd.south); + json_object_set_number(root_object, "nsres", cellhd.ns_res); + + json_object_set_number(root_object, "east", cellhd.east); + json_object_set_number(root_object, "west", cellhd.west); + json_object_set_number(root_object, "ewres", cellhd.ew_res); + + json_object_set_number(root_object, "rows", cellhd.rows); + json_object_set_number(root_object, "cols", cellhd.cols); + json_object_set_number(root_object, "cells", total_cells); + + json_object_set_string(root_object, "datatype", data_type_f); + if (cats_ok) { + json_object_set_number(root_object, "ncats", cats.num); + } else { + json_object_set_null(root_object, "ncats"); + } + break; + } } if (rflag->answer || sflag->answer) { @@ -480,12 +527,28 @@ int main(int argc, char **argv) Rast_get_range_min_max(&crange, &min, &max); if (Rast_is_c_null_value(&min)) { - fprintf(out, "min=NULL\n"); - fprintf(out, "max=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "min=NULL\n"); + fprintf(out, "max=NULL\n"); + break; + case JSON: + json_object_set_null(root_object, "min"); + json_object_set_null(root_object, "max"); + break; + } } else { - fprintf(out, "min=%i\n", min); - fprintf(out, "max=%i\n", max); + switch (format) { + case PLAIN: + fprintf(out, "min=%i\n", min); + fprintf(out, "max=%i\n", max); + break; + case JSON: + json_object_set_number(root_object, "min", min); + json_object_set_number(root_object, "max", max); + break; + } } } else { @@ -493,17 +556,33 @@ int main(int argc, char **argv) Rast_get_fp_range_min_max(&range, &min, &max); if (Rast_is_d_null_value(&min)) { - fprintf(out, "min=NULL\n"); - fprintf(out, "max=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "min=NULL\n"); + fprintf(out, "max=NULL\n"); + break; + case JSON: + json_object_set_null(root_object, "min"); + json_object_set_null(root_object, "max"); + break; + } } else { - if (data_type == FCELL_TYPE) { - fprintf(out, "min=%.7g\n", min); - fprintf(out, "max=%.7g\n", max); - } - else { - fprintf(out, "min=%.15g\n", min); - fprintf(out, "max=%.15g\n", max); + switch (format) { + case PLAIN: + if (data_type == FCELL_TYPE) { + fprintf(out, "min=%.7g\n", min); + fprintf(out, "max=%.7g\n", max); + } + else { + fprintf(out, "min=%.15g\n", min); + fprintf(out, "max=%.15g\n", max); + } + break; + case JSON: + json_object_set_number(root_object, "min", min); + json_object_set_number(root_object, "max", max); + break; } } } @@ -512,9 +591,17 @@ int main(int argc, char **argv) if (sflag->answer) { if (!gflag->answer) { + grass_int64 total_cells = (grass_int64)cellhd.rows * cellhd.cols; /* always report total number of cells */ - fprintf(out, "cells=%" PRId64 "\n", - (grass_int64)cellhd.rows * cellhd.cols); + switch (format) { + case PLAIN: + fprintf(out, "cells=%" PRId64 "\n", total_cells); + break; + case JSON: + json_object_set_number(root_object, "cells", total_cells); + break; + } + } if (rstats.count > 0) { @@ -542,83 +629,208 @@ int main(int argc, char **argv) } } - fprintf(out, "n=%" PRId64 "\n", rstats.count); - fprintf(out, "mean=%.15g\n", mean); - fprintf(out, "stddev=%.15g\n", sd); - fprintf(out, "sum=%.15g\n", rstats.sum); + switch (format) { + case PLAIN: + fprintf(out, "n=%" PRId64 "\n", rstats.count); + fprintf(out, "mean=%.15g\n", mean); + fprintf(out, "stddev=%.15g\n", sd); + fprintf(out, "sum=%.15g\n", rstats.sum); + break; + case JSON: + json_object_set_number(root_object, "n", rstats.count); + json_object_set_number(root_object, "mean", mean); + json_object_set_number(root_object, "sd", sd); + json_object_set_number(root_object, "sum", rstats.sum); + break; + } } else { - fprintf(out, "n=0\n"); - fprintf(out, "mean=NULL\n"); - fprintf(out, "stddev=NULL\n"); - fprintf(out, "sum=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "n=0\n"); + fprintf(out, "mean=NULL\n"); + fprintf(out, "stddev=NULL\n"); + fprintf(out, "sum=NULL\n"); + break; + case JSON: + json_object_set_number(root_object, "n", 0); + json_object_set_null(root_object, "mean"); + json_object_set_null(root_object, "stddev"); + json_object_set_null(root_object, "sum"); + break; + } } } if (eflag->answer) { char xname[GNAME_MAX], xmapset[GMAPSET_MAX]; + const char *maptype, *date, *creator; G_unqualified_name(name, mapset, xname, xmapset); - fprintf(out, "map=%s\n", xname); - fprintf(out, "maptype=%s\n", - hist_ok ? Rast_get_history(&hist, HIST_MAPTYPE) : "??"); - fprintf(out, "mapset=%s\n", mapset); - fprintf(out, "location=%s\n", G_location()); - fprintf(out, "project=%s\n", G_location()); - fprintf(out, "database=%s\n", G_gisdbase()); - fprintf(out, "date=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_MAPID) : "??"); - fprintf(out, "creator=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_CREATOR) : "??"); - fprintf(out, "title=\"%s\"\n", title); + maptype = hist_ok ? Rast_get_history(&hist, HIST_MAPTYPE) : "??"; + date = hist_ok ? Rast_get_history(&hist, HIST_MAPID) : "??"; + creator = hist_ok ? Rast_get_history(&hist, HIST_CREATOR) : "??"; + + switch (format) { + case PLAIN: + fprintf(out, "map=%s\n", xname); + fprintf(out, "maptype=%s\n", maptype); + fprintf(out, "mapset=%s\n", mapset); + fprintf(out, "location=%s\n", G_location()); + fprintf(out, "project=%s\n", G_location()); + fprintf(out, "database=%s\n", G_gisdbase()); + fprintf(out, "date=\"%s\"\n", date); + fprintf(out, "creator=\"%s\"\n", creator); + fprintf(out, "title=\"%s\"\n", title); + break; + case JSON: + json_object_set_string(root_object, "map", name); + json_object_set_string(root_object, "maptype", maptype); + json_object_set_string(root_object, "mapset", mapset); + json_object_set_string(root_object, "location", G_location()); + json_object_set_string(root_object, "project", G_location()); + json_object_set_string(root_object, "database", G_gisdbase()); + json_object_set_string(root_object, "date", date); + json_object_set_string(root_object, "creator", creator); + json_object_set_string(root_object, "title", title); + break; + } if (time_ok && (first_time_ok || second_time_ok)) { G_format_timestamp(&ts, timebuff); - - /*Create the r.info timestamp string */ - fprintf(out, "timestamp=\"%s\"\n", timebuff); + switch (format) { + case PLAIN: + /*Create the r.info timestamp string */ + fprintf(out, "timestamp=\"%s\"\n", timebuff); + break; + case JSON: + json_object_set_string(root_object, "timestamp", timebuff); + break; + } } else { - fprintf(out, "timestamp=\"none\"\n"); + switch (format) { + case PLAIN: + fprintf(out, "timestamp=\"none\"\n"); + break; + case JSON: + json_object_set_null(root_object, "timestamp"); + break; + } } - fprintf(out, "units=%s\n", units ? units : "\"none\""); - fprintf(out, "vdatum=%s\n", vdatum ? vdatum : "\"none\""); - fprintf(out, "semantic_label=%s\n", - semantic_label ? semantic_label : "\"none\""); - fprintf(out, "source1=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_DATSRC_1) - : "\"none\""); - fprintf(out, "source2=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_DATSRC_2) - : "\"none\""); - fprintf(out, "description=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_KEYWRD) - : "\"none\""); - if (Rast_history_length(&hist)) { - fprintf(out, "comments=\""); - for (i = 0; i < Rast_history_length(&hist); i++) - fprintf(out, "%s", Rast_history_line(&hist, i)); - fprintf(out, "\"\n"); + + switch (format) { + case PLAIN: + fprintf(out, "units=%s\n", units ? units : "\"none\""); + fprintf(out, "vdatum=%s\n", vdatum ? vdatum : "\"none\""); + fprintf(out, "semantic_label=%s\n", + semantic_label ? semantic_label : "\"none\""); + fprintf(out, "source1=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_DATSRC_1) + : "\"none\""); + fprintf(out, "source2=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_DATSRC_2) + : "\"none\""); + fprintf(out, "description=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_KEYWRD) + : "\"none\""); + if (Rast_history_length(&hist)) { + fprintf(out, "comments=\""); + for (i = 0; i < Rast_history_length(&hist); i++) + fprintf(out, "%s", Rast_history_line(&hist, i)); + fprintf(out, "\"\n"); + } + break; + case JSON: + if (units) { + json_object_set_string(root_object, "units", units); + } + else { + json_object_set_null(root_object, "units"); + } + if (vdatum) { + json_object_set_string(root_object, "vdatum", vdatum); + } + else { + json_object_set_null(root_object, "vdatum"); + } + if (semantic_label) { + json_object_set_string(root_object, "semantic_label", semantic_label); + } else { + json_object_set_null(root_object, "semantic_label"); + } + + if (hist_ok) { + json_object_set_string(root_object, "source1", Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string(root_object, "source2", Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string(root_object, "description", Rast_get_history(&hist, HIST_KEYWRD)); + JSON_Value *comments_value = json_value_init_array(); + JSON_Array *comments = json_array(comments_value); + if (Rast_history_length(&hist)) { + for (i = 0; i < Rast_history_length(&hist); i++) { + json_array_append_string(comments, Rast_history_line(&hist, i)); + } + } + json_object_set_value(root_object, "comments", comments_value); + } else { + json_object_set_null(root_object, "source1"); + json_object_set_null(root_object, "source2"); + json_object_set_null(root_object, "description"); + json_object_set_null(root_object, "comments"); + } + break; } } if (hflag->answer) { if (hist_ok) { - fprintf(out, "Data Source:\n"); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_DATSRC_1)); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_DATSRC_2)); - fprintf(out, "Data Description:\n"); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_KEYWRD)); - if (Rast_history_length(&hist)) { - fprintf(out, "Comments:\n"); - for (i = 0; i < Rast_history_length(&hist); i++) - fprintf(out, " %s\n", Rast_history_line(&hist, i)); + switch (format) { + case PLAIN: + fprintf(out, "Data Source:\n"); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_DATSRC_1)); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_DATSRC_2)); + fprintf(out, "Data Description:\n"); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_KEYWRD)); + if (Rast_history_length(&hist)) { + fprintf(out, "Comments:\n"); + for (i = 0; i < Rast_history_length(&hist); i++) + fprintf(out, " %s\n", + Rast_history_line(&hist, i)); + } + break; + case JSON: + json_object_set_string(root_object, "source1", Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string(root_object, "source2", Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string(root_object, "description", Rast_get_history(&hist, HIST_KEYWRD)); + JSON_Value *comments_value = json_value_init_array(); + JSON_Array *comments = json_array(comments_value); + if (Rast_history_length(&hist)) { + for (i = 0; i < Rast_history_length(&hist); i++) { + json_array_append_string(comments, Rast_history_line(&hist, i)); + } + } + json_object_set_value(root_object, "comments", comments_value); + break; } } } } /* else rflag or sflag or tflag or gflag or hflag or mflag */ + if (format == JSON) { + char *serialized_string = NULL; + serialized_string = json_serialize_to_string_pretty(root_value); + if (serialized_string == NULL) { + G_fatal_error(_("Failed to initialize pretty JSON string.")); + } + puts(serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); + } + return EXIT_SUCCESS; } diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index a4d554232dd..950703f996f 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -8,6 +8,7 @@ License (>=v2). Read the file COPYING that comes with GRASS for details. """ +import json from grass.gunittest.case import TestCase from grass.gunittest.gmodules import SimpleModule @@ -67,6 +68,84 @@ def test_flagh(self): """Testing flag h with map zipcodes""" self.assertModule("r.info", map="lakes", flags="h") + def test_flagg_json(self): + """Testing flag g with map geology_30m using simple module in json format""" + expected = { + "north": 228500.0, + "south": 215000.0, + "east": 645000.0, + "west": 630000.0, + "nsres": 10.0, + "ewres": 10.0, + "rows": 1350.0, + "cols": 1500.0, + "cells": 2025000.0, + "datatype": "CELL", + "ncats": 43600.0 + } + module = SimpleModule("r.info", map="lakes", flags="g", format="json") + self.runModule(module) + result = json.loads(module.outputs.stdout) + self.assertDictEqual(expected, result) + + def test_flagr_json(self): + """Testing flag r with map landcover_1m using simple module in json format""" + expected = { + "min": 34300.0, + "max": 43600.0 + } + module = SimpleModule("r.info", map="lakes", flags="r", format="json") + self.runModule(module) + result = json.loads(module.outputs.stdout) + self.assertDictEqual(expected, result) + + def test_flage_json(self): + """Testing flag e with map lsat7_2002_50 in json format """ + expected = { + "map": "lakes", + "maptype": "raster", + "mapset": "PERMANENT", + "location": "nc_spm_08_grass7", + "project": "nc_spm_08_grass7", + "date": "Fri Jan 19 23:49:34 2007", + "creator": "helena", + "title": "South-West Wake county: Wake county lakes", + "timestamp": None, + "units": None, + "vdatum": None, + "semantic_label": None, + "source1": "", + "source2": "", + "description": "generated by r.mapcalc", + "comments": [ + "1 * lakes_large" + ] + } + + module = SimpleModule("r.info", map="lakes", flags="e", format="json") + self.runModule(module) + result = json.loads(module.outputs.stdout) + + # database value will vary with the Grass sample data path + self.assertIn("database", result) + result.pop("database") + self.assertDictEqual(expected, result) + + def test_flagh_json(self): + """Testing flag h with map zipcodes in json format """ + expected = { + "source1": "", + "source2": "", + "description": "generated by r.mapcalc", + "comments": [ + "1 * lakes_large" + ] + } + module = SimpleModule("r.info", map="lakes", flags="h", format="json") + self.runModule(module) + result = json.loads(module.outputs.stdout) + self.assertDictEqual(expected, result) + if __name__ == "__main__": from grass.gunittest.main import test From 819e55c36a4a7fe148634f48990bc7e0edbf8d75 Mon Sep 17 00:00:00 2001 From: Kriti Birda <164247895+kritibirda26@users.noreply.github.com> Date: Tue, 28 May 2024 01:22:33 +0530 Subject: [PATCH 2/9] Fix lint errors Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- raster/r.info/main.c | 60 +++++++++++++++++--------- raster/r.info/testsuite/test_r_info.py | 20 +++------ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/raster/r.info/main.c b/raster/r.info/main.c index a30f5c42573..f35a60fe825 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -462,11 +462,12 @@ int main(int argc, char **argv) } if (gflag->answer) { - const char* data_type_f = (data_type == CELL_TYPE - ? "CELL" - : (data_type == DCELL_TYPE - ? "DCELL" - : (data_type == FCELL_TYPE ? "FCELL" : "??"))); + const char *data_type_f = + (data_type == CELL_TYPE + ? "CELL" + : (data_type == DCELL_TYPE + ? "DCELL" + : (data_type == FCELL_TYPE ? "FCELL" : "??"))); grass_int64 total_cells = (grass_int64)cellhd.rows * cellhd.cols; switch (format) { @@ -514,7 +515,8 @@ int main(int argc, char **argv) json_object_set_string(root_object, "datatype", data_type_f); if (cats_ok) { json_object_set_number(root_object, "ncats", cats.num); - } else { + } + else { json_object_set_null(root_object, "ncats"); } break; @@ -591,7 +593,8 @@ int main(int argc, char **argv) if (sflag->answer) { if (!gflag->answer) { - grass_int64 total_cells = (grass_int64)cellhd.rows * cellhd.cols; + grass_int64 total_cells = + (grass_int64)cellhd.rows * cellhd.cols; /* always report total number of cells */ switch (format) { case PLAIN: @@ -601,7 +604,6 @@ int main(int argc, char **argv) json_object_set_number(root_object, "cells", total_cells); break; } - } if (rstats.count > 0) { @@ -756,24 +758,35 @@ int main(int argc, char **argv) json_object_set_null(root_object, "vdatum"); } if (semantic_label) { - json_object_set_string(root_object, "semantic_label", semantic_label); - } else { + json_object_set_string(root_object, "semantic_label", + semantic_label); + } + else { json_object_set_null(root_object, "semantic_label"); } if (hist_ok) { - json_object_set_string(root_object, "source1", Rast_get_history(&hist, HIST_DATSRC_1)); - json_object_set_string(root_object, "source2", Rast_get_history(&hist, HIST_DATSRC_2)); - json_object_set_string(root_object, "description", Rast_get_history(&hist, HIST_KEYWRD)); + json_object_set_string( + root_object, "source1", + Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string( + root_object, "source2", + Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string( + root_object, "description", + Rast_get_history(&hist, HIST_KEYWRD)); JSON_Value *comments_value = json_value_init_array(); JSON_Array *comments = json_array(comments_value); if (Rast_history_length(&hist)) { for (i = 0; i < Rast_history_length(&hist); i++) { - json_array_append_string(comments, Rast_history_line(&hist, i)); + json_array_append_string( + comments, Rast_history_line(&hist, i)); } } - json_object_set_value(root_object, "comments", comments_value); - } else { + json_object_set_value(root_object, "comments", + comments_value); + } + else { json_object_set_null(root_object, "source1"); json_object_set_null(root_object, "source2"); json_object_set_null(root_object, "description"); @@ -803,9 +816,15 @@ int main(int argc, char **argv) } break; case JSON: - json_object_set_string(root_object, "source1", Rast_get_history(&hist, HIST_DATSRC_1)); - json_object_set_string(root_object, "source2", Rast_get_history(&hist, HIST_DATSRC_2)); - json_object_set_string(root_object, "description", Rast_get_history(&hist, HIST_KEYWRD)); + json_object_set_string( + root_object, "source1", + Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string( + root_object, "source2", + Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string( + root_object, "description", + Rast_get_history(&hist, HIST_KEYWRD)); JSON_Value *comments_value = json_value_init_array(); JSON_Array *comments = json_array(comments_value); if (Rast_history_length(&hist)) { @@ -813,7 +832,8 @@ int main(int argc, char **argv) json_array_append_string(comments, Rast_history_line(&hist, i)); } } - json_object_set_value(root_object, "comments", comments_value); + json_object_set_value(root_object, "comments", + comments_value); break; } } diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 950703f996f..7ca0df296e5 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -8,6 +8,7 @@ License (>=v2). Read the file COPYING that comes with GRASS for details. """ + import json from grass.gunittest.case import TestCase @@ -81,7 +82,7 @@ def test_flagg_json(self): "cols": 1500.0, "cells": 2025000.0, "datatype": "CELL", - "ncats": 43600.0 + "ncats": 43600.0, } module = SimpleModule("r.info", map="lakes", flags="g", format="json") self.runModule(module) @@ -90,17 +91,14 @@ def test_flagg_json(self): def test_flagr_json(self): """Testing flag r with map landcover_1m using simple module in json format""" - expected = { - "min": 34300.0, - "max": 43600.0 - } + expected = {"min": 34300.0, "max": 43600.0} module = SimpleModule("r.info", map="lakes", flags="r", format="json") self.runModule(module) result = json.loads(module.outputs.stdout) self.assertDictEqual(expected, result) def test_flage_json(self): - """Testing flag e with map lsat7_2002_50 in json format """ + """Testing flag e with map lsat7_2002_50 in json format""" expected = { "map": "lakes", "maptype": "raster", @@ -117,9 +115,7 @@ def test_flage_json(self): "source1": "", "source2": "", "description": "generated by r.mapcalc", - "comments": [ - "1 * lakes_large" - ] + "comments": ["1 * lakes_large"], } module = SimpleModule("r.info", map="lakes", flags="e", format="json") @@ -132,14 +128,12 @@ def test_flage_json(self): self.assertDictEqual(expected, result) def test_flagh_json(self): - """Testing flag h with map zipcodes in json format """ + """Testing flag h with map zipcodes in json format""" expected = { "source1": "", "source2": "", "description": "generated by r.mapcalc", - "comments": [ - "1 * lakes_large" - ] + "comments": ["1 * lakes_large"], } module = SimpleModule("r.info", map="lakes", flags="h", format="json") self.runModule(module) From 61161ea8e0616b4dcf6b7ab2f5f9c381343a84fd Mon Sep 17 00:00:00 2001 From: Kriti Birda <164247895+kritibirda26@users.noreply.github.com> Date: Tue, 28 May 2024 20:54:58 +0530 Subject: [PATCH 3/9] Update raster/r.info/main.c Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- raster/r.info/main.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raster/r.info/main.c b/raster/r.info/main.c index f35a60fe825..2e79065a04b 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -829,7 +829,8 @@ int main(int argc, char **argv) JSON_Array *comments = json_array(comments_value); if (Rast_history_length(&hist)) { for (i = 0; i < Rast_history_length(&hist); i++) { - json_array_append_string(comments, Rast_history_line(&hist, i)); + json_array_append_string( + comments, Rast_history_line(&hist, i)); } } json_object_set_value(root_object, "comments", From e14b8bffb662c6799b5edf31bdb78b8786115ef6 Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Tue, 28 May 2024 23:31:52 +0530 Subject: [PATCH 4/9] Always output all fields when format=JSON is specified --- raster/r.info/main.c | 16 ++++---- raster/r.info/testsuite/test_r_info.py | 55 ++++++++------------------ 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/raster/r.info/main.c b/raster/r.info/main.c index 2e79065a04b..f59b8214b95 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -384,8 +384,8 @@ int main(int argc, char **argv) else { /* g, r, s, e, or h flags */ int need_range, have_range, need_stats, have_stats; - need_range = rflag->answer; - need_stats = sflag->answer; + need_range = rflag->answer || format == JSON; + need_stats = sflag->answer || format == JSON; if (need_stats) need_range = 1; @@ -461,7 +461,7 @@ int main(int argc, char **argv) } } - if (gflag->answer) { + if (gflag->answer || format == JSON) { const char *data_type_f = (data_type == CELL_TYPE ? "CELL" @@ -523,7 +523,7 @@ int main(int argc, char **argv) } } - if (rflag->answer || sflag->answer) { + if (rflag->answer || sflag->answer || format == JSON) { if (data_type == CELL_TYPE) { CELL min, max; @@ -590,7 +590,7 @@ int main(int argc, char **argv) } } - if (sflag->answer) { + if (sflag->answer || format == JSON) { if (!gflag->answer) { grass_int64 total_cells = @@ -641,7 +641,7 @@ int main(int argc, char **argv) case JSON: json_object_set_number(root_object, "n", rstats.count); json_object_set_number(root_object, "mean", mean); - json_object_set_number(root_object, "sd", sd); + json_object_set_number(root_object, "stddev", sd); json_object_set_number(root_object, "sum", rstats.sum); break; } @@ -664,7 +664,7 @@ int main(int argc, char **argv) } } - if (eflag->answer) { + if (eflag->answer || format == JSON) { char xname[GNAME_MAX], xmapset[GMAPSET_MAX]; const char *maptype, *date, *creator; @@ -796,7 +796,7 @@ int main(int argc, char **argv) } } - if (hflag->answer) { + if (hflag->answer || format == JSON) { if (hist_ok) { switch (format) { case PLAIN: diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 7ca0df296e5..92cb66f8d69 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -69,8 +69,8 @@ def test_flagh(self): """Testing flag h with map zipcodes""" self.assertModule("r.info", map="lakes", flags="h") - def test_flagg_json(self): - """Testing flag g with map geology_30m using simple module in json format""" + def test_format_json(self): + """Testing using simple module in json format""" expected = { "north": 228500.0, "south": 215000.0, @@ -80,31 +80,18 @@ def test_flagg_json(self): "ewres": 10.0, "rows": 1350.0, "cols": 1500.0, + "n": 36011.0, + "min": 34300.0, + "max": 43600.0, + "sum": 1404513600.0, + "stddev": 739.796537, + "mean": 39002.349282, "cells": 2025000.0, "datatype": "CELL", "ncats": 43600.0, - } - module = SimpleModule("r.info", map="lakes", flags="g", format="json") - self.runModule(module) - result = json.loads(module.outputs.stdout) - self.assertDictEqual(expected, result) - - def test_flagr_json(self): - """Testing flag r with map landcover_1m using simple module in json format""" - expected = {"min": 34300.0, "max": 43600.0} - module = SimpleModule("r.info", map="lakes", flags="r", format="json") - self.runModule(module) - result = json.loads(module.outputs.stdout) - self.assertDictEqual(expected, result) - - def test_flage_json(self): - """Testing flag e with map lsat7_2002_50 in json format""" - expected = { "map": "lakes", "maptype": "raster", "mapset": "PERMANENT", - "location": "nc_spm_08_grass7", - "project": "nc_spm_08_grass7", "date": "Fri Jan 19 23:49:34 2007", "creator": "helena", "title": "South-West Wake county: Wake county lakes", @@ -117,27 +104,19 @@ def test_flage_json(self): "description": "generated by r.mapcalc", "comments": ["1 * lakes_large"], } + self.runModule(SimpleModule("r.info", map="lakes", format="plain")) - module = SimpleModule("r.info", map="lakes", flags="e", format="json") + module = SimpleModule("r.info", map="lakes", flags="g", format="json") self.runModule(module) result = json.loads(module.outputs.stdout) - # database value will vary with the Grass sample data path - self.assertIn("database", result) - result.pop("database") - self.assertDictEqual(expected, result) - - def test_flagh_json(self): - """Testing flag h with map zipcodes in json format""" - expected = { - "source1": "", - "source2": "", - "description": "generated by r.mapcalc", - "comments": ["1 * lakes_large"], - } - module = SimpleModule("r.info", map="lakes", flags="h", format="json") - self.runModule(module) - result = json.loads(module.outputs.stdout) + # the following fields vary with the Grass sample data's path + # therefore only check for their presence in the JSON output + # and not exact values + remove_fields = ["location", "project", "database"] + for field in remove_fields: + self.assertIn(field, result) + result.pop(field) self.assertDictEqual(expected, result) From 090c5dfc82096e8941b97e5bf21660d691077d89 Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Fri, 31 May 2024 15:38:10 +0530 Subject: [PATCH 5/9] remove redundant runModule in test --- raster/r.info/testsuite/test_r_info.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 92cb66f8d69..34004659315 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -104,8 +104,6 @@ def test_format_json(self): "description": "generated by r.mapcalc", "comments": ["1 * lakes_large"], } - self.runModule(SimpleModule("r.info", map="lakes", format="plain")) - module = SimpleModule("r.info", map="lakes", flags="g", format="json") self.runModule(module) result = json.loads(module.outputs.stdout) From 207119e90ab168b84b89960e195fd9045b5970cc Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Fri, 7 Jun 2024 01:58:36 +0530 Subject: [PATCH 6/9] address feedback from code review --- raster/r.info/main.c | 10 ++++---- raster/r.info/testsuite/test_r_info.py | 32 +++++++++++++------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/raster/r.info/main.c b/raster/r.info/main.c index f59b8214b95..815c7ca133e 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -120,15 +120,13 @@ int main(int argc, char **argv) if ((mapset = G_find_raster2(name, "")) == NULL) G_fatal_error(_("Raster map <%s> not found"), name); - if (strcmp(fopt->answer, "json") == 0) + if (strcmp(fopt->answer, "json") == 0) { format = JSON; - else - format = PLAIN; - - if (format == JSON) { root_value = json_value_init_object(); root_object = json_value_get_object(root_value); - json_set_float_serialization_format("%lf"); + } + else { + format = PLAIN; } Rast_get_cellhd(name, "", &cellhd); diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 34004659315..9596c153411 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -72,23 +72,23 @@ def test_flagh(self): def test_format_json(self): """Testing using simple module in json format""" expected = { - "north": 228500.0, - "south": 215000.0, - "east": 645000.0, - "west": 630000.0, - "nsres": 10.0, - "ewres": 10.0, - "rows": 1350.0, - "cols": 1500.0, - "n": 36011.0, - "min": 34300.0, - "max": 43600.0, - "sum": 1404513600.0, - "stddev": 739.796537, - "mean": 39002.349282, - "cells": 2025000.0, + "north": 228500, + "south": 215000, + "east": 645000, + "west": 630000, + "nsres": 10, + "ewres": 10, + "rows": 1350, + "cols": 1500, + "n": 36011, + "min": 34300, + "max": 43600, + "sum": 1404513600, + "stddev": 739.7965366431155, + "mean": 39002.349282163785, + "cells": 2025000, "datatype": "CELL", - "ncats": 43600.0, + "ncats": 43600, "map": "lakes", "maptype": "raster", "mapset": "PERMANENT", From 4bb1a8a2d7e450fb0643fe6f95d25c2219ea9e90 Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Fri, 7 Jun 2024 14:01:16 +0530 Subject: [PATCH 7/9] document json format for r.info --- raster/r.info/r.info.html | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/raster/r.info/r.info.html b/raster/r.info/r.info.html index e0ce6ff5bb3..7135ea0f05f 100644 --- a/raster/r.info/r.info.html +++ b/raster/r.info/r.info.html @@ -124,6 +124,55 @@

EXAMPLES

comments="slope map elev = elev_ned10mzfactor = 1.00 format = degreesmin_slp_allowed = 0.000000" +

+Finally, the output from r.info can be output in JSON by passing the format=json option. + +

+r.info slope format=json
+
+
+{
+    "north": 228500,
+    "south": 215000,
+    "nsres": 10,
+    "east": 645000,
+    "west": 630000,
+    "ewres": 10,
+    "rows": 1350,
+    "cols": 1500,
+    "cells": 2025000,
+    "datatype": "FCELL",
+    "ncats": 255,
+    "min": 0,
+    "max": 38.68939208984375,
+    "n": 2019304,
+    "mean": 3.864522406673347,
+    "stddev": 3.0079141222181214,
+    "sum": 7803645.5538851162,
+    "map": "slope",
+    "maptype": "raster",
+    "mapset": "PERMANENT",
+    "location": "nc_spm_08_grass7",
+    "project": "nc_spm_08_grass7",
+    "database": "/grassdata",
+    "date": "Tue Nov  7 01:11:23 2006",
+    "creator": "helena",
+    "title": "South-West Wake county: slope in degrees",
+    "timestamp": null,
+    "units": null,
+    "vdatum": null,
+    "semantic_label": null,
+    "source1": "raster elevation file elev_ned10m",
+    "source2": "",
+    "description": "generated by r.slope.aspect",
+    "comments": [
+        "slope map elev = elev_ned10m",
+        "zfactor = 1.00 format = degrees",
+        "min_slp_allowed = 0.000000"
+    ]
+}
+
+

SEE ALSO

From 34c14058687d1caead81063ad31899097b0d6efa Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Fri, 14 Jun 2024 19:39:18 +0530 Subject: [PATCH 8/9] respect sflag in json output of r.info --- raster/r.info/main.c | 4 +- raster/r.info/r.info.html | 4 -- raster/r.info/testsuite/test_r_info.py | 95 +++++++++++++++----------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/raster/r.info/main.c b/raster/r.info/main.c index 815c7ca133e..ed06f72eba7 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -383,7 +383,7 @@ int main(int argc, char **argv) int need_range, have_range, need_stats, have_stats; need_range = rflag->answer || format == JSON; - need_stats = sflag->answer || format == JSON; + need_stats = sflag->answer; if (need_stats) need_range = 1; @@ -588,7 +588,7 @@ int main(int argc, char **argv) } } - if (sflag->answer || format == JSON) { + if (sflag->answer) { if (!gflag->answer) { grass_int64 total_cells = diff --git a/raster/r.info/r.info.html b/raster/r.info/r.info.html index 7135ea0f05f..8cc7b1a429f 100644 --- a/raster/r.info/r.info.html +++ b/raster/r.info/r.info.html @@ -145,10 +145,6 @@

EXAMPLES

"ncats": 255, "min": 0, "max": 38.68939208984375, - "n": 2019304, - "mean": 3.864522406673347, - "stddev": 3.0079141222181214, - "sum": 7803645.5538851162, "map": "slope", "maptype": "raster", "mapset": "PERMANENT", diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 9596c153411..2c2da24b663 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -5,8 +5,8 @@ Author: Sunveer Singh, Google Code-in 2017 Copyright: (C) 2017 by Sunveer Singh and the GRASS Development Team Licence: This program is free software under the GNU General Public - License (>=v2). Read the file COPYING that comes with GRASS - for details. + License (>=v2). Read the file COPYING that comes with GRASS + for details. """ import json @@ -20,6 +20,35 @@ class TestReport(TestCase): def setUpClass(cls): """Use temporary region settings""" cls.use_temp_region() + cls.json_format_expected_no_stats = { + "north": 228500, + "south": 215000, + "east": 645000, + "west": 630000, + "nsres": 10, + "ewres": 10, + "rows": 1350, + "cols": 1500, + "min": 34300, + "max": 43600, + "cells": 2025000, + "datatype": "CELL", + "ncats": 43600, + "map": "lakes", + "maptype": "raster", + "mapset": "PERMANENT", + "date": "Fri Jan 19 23:49:34 2007", + "creator": "helena", + "title": "South-West Wake county: Wake county lakes", + "timestamp": None, + "units": None, + "vdatum": None, + "semantic_label": None, + "source1": "", + "source2": "", + "description": "generated by r.mapcalc", + "comments": ["1 * lakes_large"], + } @classmethod def tearDownClass(cls): @@ -69,42 +98,7 @@ def test_flagh(self): """Testing flag h with map zipcodes""" self.assertModule("r.info", map="lakes", flags="h") - def test_format_json(self): - """Testing using simple module in json format""" - expected = { - "north": 228500, - "south": 215000, - "east": 645000, - "west": 630000, - "nsres": 10, - "ewres": 10, - "rows": 1350, - "cols": 1500, - "n": 36011, - "min": 34300, - "max": 43600, - "sum": 1404513600, - "stddev": 739.7965366431155, - "mean": 39002.349282163785, - "cells": 2025000, - "datatype": "CELL", - "ncats": 43600, - "map": "lakes", - "maptype": "raster", - "mapset": "PERMANENT", - "date": "Fri Jan 19 23:49:34 2007", - "creator": "helena", - "title": "South-West Wake county: Wake county lakes", - "timestamp": None, - "units": None, - "vdatum": None, - "semantic_label": None, - "source1": "", - "source2": "", - "description": "generated by r.mapcalc", - "comments": ["1 * lakes_large"], - } - module = SimpleModule("r.info", map="lakes", flags="g", format="json") + def _test_format_json_helper(self, module, expected): self.runModule(module) result = json.loads(module.outputs.stdout) @@ -115,7 +109,30 @@ def test_format_json(self): for field in remove_fields: self.assertIn(field, result) result.pop(field) - self.assertDictEqual(expected, result) + + self.assertCountEqual(list(expected.keys()), list(result.keys())) + + for key, value in expected.items(): + if isinstance(value, float): + self.assertAlmostEqual(value, result[key]) + self.assertEqual(value, result[key]) + + def test_format_json(self): + """Testing using simple module in json format""" + module = SimpleModule("r.info", map="lakes", flags="g", format="json") + self._test_format_json_helper(module, self.json_format_expected_no_stats) + + def test_sflag_format_json(self): + """Testing using simple module in json format with sflag""" + expected_json_with_stats = { + **self.json_format_expected_no_stats, + "n": 36011, + "sum": 1404513600, + "stddev": 739.7965366431155, + "mean": 39002.349282163785, + } + module = SimpleModule("r.info", map="lakes", flags="gs", format="json") + self._test_format_json_helper(module, expected_json_with_stats) if __name__ == "__main__": From 7918821d91e605ee35b6b989f36c6de5a1139612 Mon Sep 17 00:00:00 2001 From: Kriti Birda Date: Sat, 15 Jun 2024 00:24:22 +0530 Subject: [PATCH 9/9] fix test --- raster/r.info/testsuite/test_r_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index 2c2da24b663..49f91a5723f 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -115,7 +115,8 @@ def _test_format_json_helper(self, module, expected): for key, value in expected.items(): if isinstance(value, float): self.assertAlmostEqual(value, result[key]) - self.assertEqual(value, result[key]) + else: + self.assertEqual(value, result[key]) def test_format_json(self): """Testing using simple module in json format"""