Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions vector/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ build_program_in_subdir(
grass_dbmidriver
grass_gis
grass_vector
grass_parson
${LIBM})

build_program_in_subdir(
Expand Down
2 changes: 1 addition & 1 deletion vector/v.net/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../..

PGM=v.net

LIBES = $(VECTORLIB) $(GISLIB) $(DBMILIB)
LIBES = $(VECTORLIB) $(GISLIB) $(DBMILIB) $(PARSONLIB)
DEPENDENCIES = $(VECTORDEP) $(GISDEP) $(DBMIDEP)
EXTRA_INC = $(VECT_INC)
EXTRA_CFLAGS = $(VECT_CFLAGS)
Expand Down
19 changes: 18 additions & 1 deletion vector/v.net/args.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ void define_options(struct opt *opt)
{
char *desc;

opt->format = G_define_standard_option(G_OPT_F_FORMAT);
opt->format->key = "format";
if (!opt->format->description)
opt->format->description = _("Output format");
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be any need to add these.

Suggested change
opt->format->key = "format";
if (!opt->format->description)
opt->format->description = _("Output format");

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"After removing those parts and running make, the build gets stuck at the UI description generation step with the following warnings: 'Missing option key' and 'Description for option <?> missing'.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it and it works fine for me. Look at other usages of G_OPT_F_FORMAT.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you'd need to clean before make, like make clean, make libsclean, or make distclean. With the last one, you'd need to run configure again

opt->format->descriptions = _("plain;Human readable text output;"
"json;JSON (JavaScript Object Notation)");
opt->format->options = "plain,json";
opt->format->answer = "plain";
opt->format->guisection = _("Print");

opt->input = G_define_standard_option(G_OPT_V_INPUT);
opt->input->required = NO;
opt->input->label = _("Name of input vector line map (arcs)");
Expand Down Expand Up @@ -123,7 +133,7 @@ void define_options(struct opt *opt)
}

void parse_arguments(const struct opt *opt, int *afield, int *nfield,
double *thresh, int *act)
double *thresh, int *act, enum OutputFormat *format)
{
*afield = atoi(opt->afield_opt->answer);
*nfield = atoi(opt->nfield_opt->answer);
Expand All @@ -144,6 +154,13 @@ void parse_arguments(const struct opt *opt, int *afield, int *nfield,
else
G_fatal_error(_("Unknown operation"));

if (strcmp(opt->format->answer, "json") == 0) {
*format = FORMAT_JSON;
}
else {
*format = FORMAT_PLAIN;
}

if (*act == TOOL_NODES || *act == TOOL_CONNECT || *act == TOOL_REPORT ||
*act == TOOL_NREPORT || *act == TOOL_TURNTABLE) {
if (opt->input->answer == NULL)
Expand Down
7 changes: 4 additions & 3 deletions vector/v.net/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <grass/vector.h>
#include <grass/glocale.h>
#include "proto.h"
#include <grass/gjson.h>

int main(int argc, char **argv)
{
Expand All @@ -32,10 +33,10 @@ int main(int argc, char **argv)
struct Map_info *In = NULL, *Out = NULL, *Points = NULL;

FILE *file_arcs;

int afield, nfield;
int act;
double thresh;
enum OutputFormat format;

char message[4096];

Expand All @@ -53,7 +54,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, &format);

In = Points = Out = NULL;
file_arcs = NULL;
Expand Down Expand Up @@ -172,7 +173,7 @@ int main(int argc, char **argv)
turntable(&opt);
}
else { /* report */
report(In, afield, nfield, act);
report(In, afield, nfield, act, format);
}

if (In)
Expand Down
8 changes: 6 additions & 2 deletions vector/v.net/proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ struct opt {
struct Option *file;
struct Option *type;
struct Flag *cats_flag, *snap_flag;
struct Option *format;
};

enum OutputFormat { FORMAT_PLAIN, FORMAT_JSON };

/* arcs.c */
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 *,
enum OutputFormat *);

/* connect.c */
int connect_arcs(struct Map_info *, struct Map_info *, struct Map_info *, int,
Expand All @@ -30,6 +34,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, enum OutputFormat);

void turntable(struct opt *);
94 changes: 83 additions & 11 deletions vector/v.net/report.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#include <grass/vector.h>
#include <grass/glocale.h>
#include "proto.h"
#include <grass/gjson.h>

int report(struct Map_info *In, int afield, int nfield, int action)
int report(struct Map_info *In, int afield, int nfield, int action,
enum OutputFormat format)
{
int i, j, line, nlines, ltype, node, nnodes;
int cat_line, cat_node[2];
Expand All @@ -24,6 +26,14 @@ 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 == FORMAT_JSON) {
root_value = G_json_value_init_array();
root_array = G_json_array(root_value);
}

List = Vect_new_boxlist(0);

/* For all lines find categories for points on nodes */
Expand Down Expand Up @@ -67,8 +77,32 @@ 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 (format == FORMAT_JSON) {
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 (format == FORMAT_JSON) {
char *json_str = G_json_serialize_to_string_pretty(root_value);
if (json_str) {
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 */
Expand All @@ -77,6 +111,14 @@ int report(struct Map_info *In, int afield, int nfield, int action)

List = Vect_new_list();

G_JSON_Value *root_val = NULL;
G_JSON_Array *root_arr = NULL;

if (format == FORMAT_JSON) {
root_val = G_json_value_init_array();
root_arr = G_json_array(root_val);
}

for (i = 1; i <= nlines; i++) {

if (Vect_get_line_type(In, i) != GV_POINT)
Expand All @@ -102,9 +144,19 @@ int report(struct Map_info *In, int afield, int nfield, int action)
for (j = 0; j < Cats->n_cats; j++) {
if (Cats->field[j] == nfield) {
int count = 0;

fprintf(stdout, "%d ", Cats->cat[j]);

G_JSON_Value *item_val =
root_arr ? G_json_value_init_object() : NULL;
G_JSON_Value *lines_val =
root_arr ? G_json_value_init_array() : NULL;

if (format == FORMAT_JSON) {
G_json_object_set_number(
G_json_value_get_object(item_val), "node_cat",
Cats->cat[j]);
}
else {
fprintf(stdout, "%d ", Cats->cat[j]);
}
/* Loop through all lines */
for (k = 0; k < nelem; k++) {
elem = abs(Vect_get_node_line(In, node, k));
Expand All @@ -115,19 +167,39 @@ int report(struct Map_info *In, int afield, int nfield, int action)
/* Loop through all cats of line */
for (l = 0; l < Cats2->n_cats; l++) {
if (Cats2->field[l] == afield) {
if (count > 0)
fprintf(stdout, ",");

fprintf(stdout, "%d", Cats2->cat[l]);
count++;
if (format == FORMAT_JSON)
G_json_array_append_number(
G_json_array(lines_val),
Cats2->cat[l]);
else {
if (count > 0)
fprintf(stdout, ",");
fprintf(stdout, "%d", Cats2->cat[l]);
count++;
}
}
}
}
fprintf(stdout, "\n");
if (format == FORMAT_JSON) {
G_json_object_set_value(
G_json_value_get_object(item_val), "lines",
lines_val);
G_json_array_append_value(root_arr, item_val);
}
else
fprintf(stdout, "\n");
}
}
}
}
if (format == FORMAT_JSON) {
char *s = G_json_serialize_to_string_pretty(root_val);
if (s) {
fprintf(stdout, "%s\n", s);
G_json_free_serialized_string(s);
}
G_json_value_free(root_val);
}
Vect_destroy_list(List);
}
Vect_destroy_cats_struct(Cats);
Expand Down
58 changes: 57 additions & 1 deletion vector/v.net/testsuite/test_v_net.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you double check which of the tests take long? In the error logs, these 6 tests in the file took 69 seconds to run on Ubuntu, which is quite long in my opinion. Maybe they were already long, but take a look to see if it is the newly added tests.

About 38.4sec on macOS (the Apple silicon chips are really fast, so it's normal, about 2x as fast as the Linux runners usually).

66 sec for the Linux CMake

Windows didn't finish run it yet.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've optimized the tests to address the execution time concerns. The v.net network preparation steps are now moved to setUpClass, so they only run once for the entire test suite. My local tests show a significant reduction in total runtime. I've also ensured that existing tests and the tearDown logic remain intact.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the before and after (approx), and what was the other tests to compare to?

Copy link
Author

@tangelll tangelll Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized I was preparing the heavy streets dataset twice (once for each new test). I have now moved the network preparation to setUpClass so it only runs once. This fixed the slowdown. Before it was 38 sec. in my local mac now it is 28 sec. in my local mac.

Original file line number Diff line number Diff line change
@@ -1,16 +1,72 @@
from grass.gunittest.case import TestCase
from grass.gunittest.main import test
from grass.script.core import read_command
from grass.gunittest.gmodules import SimpleModule
import json


class TestVNet(TestCase):
network = "test_vnet"

def tearDown(self):
"""Remove viewshed map after each test method"""
# TODO: eventually, removing maps should be handled through testing framework functions
self.runModule("g.remove", flags="f", type="vector", name=self.network)

def test_nreport_json_output(self):
"""Verify that v.net nreport produces valid and accurate JSON output"""
self.runModule(
"v.net",
input="streets",
points="schools",
output=self.network,
operation="connect",
threshold=400,
flags="c",
)

vnet_module = SimpleModule(
"v.net",
input=self.network,
operation="nreport",
node_layer=2,
format="json",
)
self.assertModule(vnet_module)

actual_output = json.loads(vnet_module.outputs.stdout)

self.assertIsInstance(actual_output, list)
self.assertGreater(len(actual_output), 0, "The JSON output list is empty")

expected_keys = {"node_cat", "lines"}
self.assertEqual(set(actual_output[0].keys()), expected_keys)
self.assertIsInstance(actual_output[0]["node_cat"], int)
self.assertIsInstance(actual_output[0]["lines"], list)

def test_report_json_output(self):
"""Verify that v.net report produces valid and accurate JSON output"""
vnet_module = SimpleModule(
"v.net",
input="streets_net",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this found? It doesn't seem to be in the test dataset, or created just for the test, as the same tests fail on all platforms

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, streets_net was part of my local North Carolina sample dataset and I mistakenly assumed it was available in the test environment. I will update the test to either use a standard test dataset.Sorry for the oversight!

operation="report",
arc_layer=1,
node_layer=2,
format="json",
)
self.assertModule(vnet_module)

actual_output = json.loads(vnet_module.outputs.stdout)

self.assertIsInstance(actual_output, list)
self.assertGreater(len(actual_output), 0, "The JSON output list is empty")

expected_keys = {"line_cat", "start_node_cat", "end_node_cat"}
self.assertEqual(set(actual_output[0].keys()), expected_keys)

for entry in actual_output[:10]:
for key in expected_keys:
self.assertIsInstance(entry[key], int)

def test_nodes(self):
"""Test"""
self.assertModule(
Expand Down
Loading
Loading