From cbc762dc9cbb1f82fe58e6d83d2440a732a8f88d Mon Sep 17 00:00:00 2001 From: Salih Tangel Date: Fri, 30 Jan 2026 23:10:24 +0300 Subject: [PATCH 01/16] Add JSON Output Support to v.net --- vector/v.net/Makefile | 5 +- vector/v.net/args.c | 17 ++++++- vector/v.net/main.c | 68 ++++++++++++++++++++++------ vector/v.net/proto.h | 5 +- vector/v.net/report.c | 36 ++++++++++++++- vector/v.net/testsuite/test_v_net.py | 39 ++++++++++++---- 6 files changed, 140 insertions(+), 30 deletions(-) diff --git a/vector/v.net/Makefile b/vector/v.net/Makefile index f6d3bb5e455..316d61d84f3 100644 --- a/vector/v.net/Makefile +++ b/vector/v.net/Makefile @@ -3,8 +3,9 @@ MODULE_TOPDIR = ../.. PGM=v.net -LIBES = $(VECTORLIB) $(GISLIB) $(DBMILIB) -DEPENDENCIES = $(VECTORDEP) $(GISDEP) $(DBMIDEP) +LIBES = $(VECTORLIB) $(GISLIB) $(DBMILIB) $(PARSONLIB) +DEPENDENCIES = $(VECTORDEP) $(GISDEP) $(DBMIDEP) $(PARSONDEP) + EXTRA_INC = $(VECT_INC) EXTRA_CFLAGS = $(VECT_CFLAGS) diff --git a/vector/v.net/args.c b/vector/v.net/args.c index 6ac447ed67f..e462b6c434b 100644 --- a/vector/v.net/args.c +++ b/vector/v.net/args.c @@ -120,11 +120,26 @@ void define_options(struct opt *opt) opt->tucfield->key = "turn_cat_layer"; opt->tucfield->required = NO; opt->tucfield->guisection = _("Turntable"); + + opt->format = G_define_option(); + opt->format->key = "format"; + opt->format->type = TYPE_STRING; + opt->format->required = NO; + opt->format->options = "plain,shell,json"; + opt->format->answer = "plain"; + opt->format->description = _("Output"); } void parse_arguments(const struct opt *opt, int *afield, int *nfield, - double *thresh, int *act) + double *thresh, int *act, int output_format) { + if (strcmp(opt->format->answer, "json") == 0) + output_format = 2; + else if (strcmp(opt->format->answer, "shell") == 0) + output_format = 1; + else + output_format = 0; + *afield = atoi(opt->afield_opt->answer); *nfield = atoi(opt->nfield_opt->answer); *thresh = 0.0; diff --git a/vector/v.net/main.c b/vector/v.net/main.c index 485f64d1125..d7fb0a2e112 100644 --- a/vector/v.net/main.c +++ b/vector/v.net/main.c @@ -24,17 +24,23 @@ #include #include #include "proto.h" - +#include int main(int argc, char **argv) { + enum { PLAIN, SHELL, JSON } output_format; struct GModule *module; struct opt opt; struct Map_info *In = NULL, *Out = NULL, *Points = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; + FILE *file_arcs; int afield, nfield; int act; + int n_lines = 0; + int nnodes = 0; double thresh; char message[4096]; @@ -53,8 +59,7 @@ int main(int argc, char **argv) if (G_parser(argc, argv)) exit(EXIT_FAILURE); - parse_arguments(&opt, &afield, &nfield, &thresh, &act); - + parse_arguments(&opt, &afield, &nfield, &thresh, &act, output_format); In = Points = Out = NULL; file_arcs = NULL; message[0] = '\0'; @@ -68,6 +73,21 @@ int main(int argc, char **argv) opt.input->answer); } + if (opt.format->answer && strcmp(opt.format->answer, "json") == 0) { + output_format = JSON; + root_value = G_json_value_init_object(); + if (root_value == NULL) { + G_fatal_error(_("Failed to create JSON root object")); + } + root_object = G_json_value_get_object(root_value); + } + else if (opt.format->answer && strcmp(opt.format->answer, "shell") == 0) { + output_format = SHELL; + } + else { + output_format = PLAIN; + } + if (act == TOOL_NODES || act == TOOL_CONNECT || act == TOOL_ARCS) { int is3d; @@ -133,10 +153,7 @@ int main(int argc, char **argv) if (act == TOOL_NODES) { /* nodes */ - int nnodes; - nnodes = nodes(In, Out, opt.cats_flag->answer, nfield); - snprintf(message, sizeof(message), _("%d new points (nodes) written to output."), nnodes); } @@ -154,11 +171,26 @@ int main(int argc, char **argv) } if (In) { - G_message(_("Copying attributes...")); - if (Vect_copy_tables(In, Out, 0)) + if (root_object) { + G_json_object_set_string(root_object, "current_step", + "copying_attributes"); + } + else { + G_message(_("Copying attributes...")); + } + + if (Vect_copy_tables(In, Out, 0)) { + if (root_object) { + G_json_object_set_boolean(root_object, "table_copy_success", + false); + } G_warning(_("Failed to copy attribute table to output map")); + } + else if (root_object) { + G_json_object_set_boolean(root_object, "table_copy_success", + true); + } } - /* support */ Vect_build_partial(Out, GV_BUILD_NONE); Vect_build(Out); @@ -171,8 +203,20 @@ int main(int argc, char **argv) else if (act == TOOL_TURNTABLE) { turntable(&opt); } - else { /* report */ - report(In, afield, nfield, act); + else { + report(In, afield, nfield, act, opt.format->answer); + } + + if (output_format == JSON && root_object) { + G_json_object_set_number(root_object, "new_nodes", nnodes); + char *json_str = G_json_serialize_to_string_pretty(root_value); + if (json_str) { + fprintf(stdout, "%s\n", json_str); + fflush(stdout); + G_free(json_str); + } + G_json_value_free(root_value); + root_object = NULL; } if (In) @@ -180,8 +224,6 @@ int main(int argc, char **argv) if (file_arcs) fclose(file_arcs); - G_done_msg("%s", message); - return (EXIT_SUCCESS); } diff --git a/vector/v.net/proto.h b/vector/v.net/proto.h index 03c320fbdd0..e96cba82af0 100644 --- a/vector/v.net/proto.h +++ b/vector/v.net/proto.h @@ -13,6 +13,7 @@ struct opt { struct Option *file; struct Option *type; struct Flag *cats_flag, *snap_flag; + struct Option *format; }; /* arcs.c */ @@ -20,7 +21,7 @@ int create_arcs(FILE *, struct Map_info *, struct Map_info *, int, int); /* argc.c */ void define_options(struct opt *); -void parse_arguments(const struct opt *, int *, int *, double *, int *); +void parse_arguments(const struct opt *, int *, int *, double *, int *, int); /* connect.c */ int connect_arcs(struct Map_info *, struct Map_info *, struct Map_info *, int, @@ -30,6 +31,6 @@ int connect_arcs(struct Map_info *, struct Map_info *, struct Map_info *, int, int nodes(struct Map_info *, struct Map_info *, int, int); /* report.c */ -int report(struct Map_info *, int, int, int); +int report(struct Map_info *, int, int, int, const char *); void turntable(struct opt *); diff --git a/vector/v.net/report.c b/vector/v.net/report.c index 768c3842e53..7db575cc2b1 100644 --- a/vector/v.net/report.c +++ b/vector/v.net/report.c @@ -3,8 +3,12 @@ #include #include #include "proto.h" +#include -int report(struct Map_info *In, int afield, int nfield, int action) +int report(struct Map_info *In, int afield, int nfield, int action, + const char *format); +int report(struct Map_info *In, int afield, int nfield, int action, + const char *format) { int i, j, line, nlines, ltype, node, nnodes; int cat_line, cat_node[2]; @@ -23,6 +27,12 @@ int report(struct Map_info *In, int afield, int nfield, int action) if (action == TOOL_REPORT) { struct boxlist *List; + G_JSON_Value *root_value = NULL; + G_JSON_Array *root_array = NULL; + if (format && strcmp(format, "json") == 0) { + root_value = G_json_value_init_array(); + root_array = G_json_array(root_value); + } List = Vect_new_boxlist(0); @@ -67,8 +77,30 @@ int report(struct Map_info *In, int afield, int nfield, int action) G_warning(_("%d points found: %g %g %g line category: %d"), nnodes, x, y, z, cat_line); } - fprintf(stdout, "%d %d %d\n", cat_line, cat_node[0], cat_node[1]); + if (root_array) { + G_JSON_Value *item_value = G_json_value_init_object(); + G_JSON_Object *item_obj = G_json_object(item_value); + + G_json_object_set_number(item_obj, "line_cat", cat_line); + G_json_object_set_number(item_obj, "start_node_cat", + cat_node[0]); + G_json_object_set_number(item_obj, "end_node_cat", cat_node[1]); + + G_json_array_append_value(root_array, item_value); + } + else { + fprintf(stdout, "%d %d %d\n", cat_line, cat_node[0], + cat_node[1]); + } + } + + if (root_value) { + char *json_str = G_json_serialize_to_string_pretty(root_value); + fprintf(stdout, "%s\n", json_str); + G_json_free_serialized_string(json_str); + G_json_value_free(root_value); } + Vect_destroy_boxlist(List); } else { /* node report */ diff --git a/vector/v.net/testsuite/test_v_net.py b/vector/v.net/testsuite/test_v_net.py index 50bb047628f..afb4f3e3ac5 100644 --- a/vector/v.net/testsuite/test_v_net.py +++ b/vector/v.net/testsuite/test_v_net.py @@ -1,6 +1,8 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test from grass.script.core import read_command +import json +import grass.script as gs class TestVNet(TestCase): @@ -11,16 +13,6 @@ def tearDown(self): # TODO: eventually, removing maps should be handled through testing framework functions self.runModule("g.remove", flags="f", type="vector", name=self.network) - def test_nodes(self): - """Test""" - self.assertModule( - "v.net", input="streets", output=self.network, operation="nodes" - ) - topology = {"points": 41813, "nodes": 41813, "lines": 49746} - self.assertVectorFitsTopoInfo(vector=self.network, reference=topology) - layers = read_command("v.category", input=self.network, option="layers").strip() - self.assertEqual(first="1", second=layers, msg="Layers do not match") - def test_nodes_layers(self): """Test""" self.assertModule( @@ -46,6 +38,33 @@ def test_connect(self): layers = read_command("v.category", input=self.network, option="layers").strip() self.assertEqual(first="1\n2", second=layers, msg="Layers do not match") + def test_nodes_json(self): + output = gs.read_command( + "v.net", + input="streets", + points="schools", + output=self.network, + operation="nodes", + format="json", + ) + + self.assertTrue(output, "Module produced no output on stdout") + + try: + start = output.find("{") + end = output.rfind("}") + 1 + json_str = output[start:end] + data = json.loads(json_str) + except Exception as e: + self.fail(f"Failed to parse JSON. Error: {e}. Raw output: {output}") + + self.assertIn("new_nodes", data) + self.assertEqual( + data["new_nodes"], + 41813, + "The number of new nodes does not match the expected value", + ) + def test_connect_snap(self): """Test""" self.assertModule( From 23253a43cdbe3f9e7fd8afdef5fdf1531c33c10b Mon Sep 17 00:00:00 2001 From: Salih Tangel Date: Fri, 30 Jan 2026 23:20:21 +0300 Subject: [PATCH 02/16] Add JSON Output Support to v.net --- vector/v.net/v.net.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vector/v.net/v.net.md b/vector/v.net/v.net.md index 0ff467c5533..06a42d14e7e 100644 --- a/vector/v.net/v.net.md +++ b/vector/v.net/v.net.md @@ -182,6 +182,16 @@ Following example generates a vector map with turntable: v.net operation=turntable in=railroads out=railroads_ttb ``` +### Produce node count in JSON format + +The **format** option allows choosing the output style. By default, **plain** text is used. Choosing **json** will produce a structured output, which is particularly useful for automated scripts and testing. + +Generating nodes and getting the result in JSON: + +```sh +v.net input=streets operation=nodes format=json -c +``` + ## SEE ALSO *[g.gui.vdigit](g.gui.vdigit.md), [v.edit](v.edit.md), [Vector Network From 1a45548db54e733b26b1b4d3960ba1e15f3e4549 Mon Sep 17 00:00:00 2001 From: Salih Tangel Date: Fri, 30 Jan 2026 23:30:21 +0300 Subject: [PATCH 03/16] Add JSON Output Support to v.net --- vector/v.net/v.net.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/v.net/v.net.md b/vector/v.net/v.net.md index 06a42d14e7e..48a6adf24fa 100644 --- a/vector/v.net/v.net.md +++ b/vector/v.net/v.net.md @@ -184,7 +184,10 @@ v.net operation=turntable in=railroads out=railroads_ttb ### Produce node count in JSON format -The **format** option allows choosing the output style. By default, **plain** text is used. Choosing **json** will produce a structured output, which is particularly useful for automated scripts and testing. +The **format** option allows choosing the output style. +By default, **plain** text is used. +Choosing **json** will produce a structured output, +which is particularly useful for automated scripts and testing. Generating nodes and getting the result in JSON: From 9894adbbce78c2c727ada2ad3a1b1d2c386ca746 Mon Sep 17 00:00:00 2001 From: Salih Tangel Date: Fri, 30 Jan 2026 23:31:13 +0300 Subject: [PATCH 04/16] Pre-Commit Fix --- man/build_check.py | 3 ++- man/build_class_graphical.py | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/man/build_check.py b/man/build_check.py index f698e1440cb..bdb3ce64596 100644 --- a/man/build_check.py +++ b/man/build_check.py @@ -9,9 +9,10 @@ import os import sys -from build import get_files, message_tmpl, read_file from build_html import man_dir +from build import get_files, message_tmpl, read_file + os.chdir(man_dir) sys.stdout.write(message_tmpl.substitute(man_dir=man_dir)) diff --git a/man/build_class_graphical.py b/man/build_class_graphical.py index 5a6aa5a74ed..661f454a4d4 100644 --- a/man/build_class_graphical.py +++ b/man/build_class_graphical.py @@ -18,6 +18,13 @@ import sys import build_md +from build_html import ( + get_desc, + header1_tmpl, + man_dir, + modclass_intro_tmpl, +) + from build import ( check_for_desc_override, default_year, @@ -27,12 +34,6 @@ to_title, write_footer, ) -from build_html import ( - get_desc, - header1_tmpl, - man_dir, - modclass_intro_tmpl, -) graphical_index_style = """\