From 8f20bc1acb14603abd6d64412c2c197a686d8f35 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Mon, 5 Jan 2026 21:01:55 +0530 Subject: [PATCH 1/9] v.profile: Added JSON output format support --- vector/v.profile/Makefile | 2 +- vector/v.profile/main.c | 339 ++++++++++++++++++++++++++++---------- 2 files changed, 251 insertions(+), 90 deletions(-) diff --git a/vector/v.profile/Makefile b/vector/v.profile/Makefile index 71c7165a2c2..61c939b637a 100644 --- a/vector/v.profile/Makefile +++ b/vector/v.profile/Makefile @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../.. PGM = v.profile -LIBES = $(VECTORLIB) $(DBMILIB) $(GISLIB) $(GEOSLIBS) +LIBES = $(VECTORLIB) $(DBMILIB) $(GISLIB) $(GEOSLIBS) $(PARSONLIB) DEPENDENCIES = $(VECTORDEP) $(DBMIDEP) $(GISDEP) EXTRA_INC = $(VECT_INC) EXTRA_CFLAGS = $(VECT_CFLAGS) $(GEOSCFLAGS) diff --git a/vector/v.profile/main.c b/vector/v.profile/main.c index 958e869e138..b120b80afd5 100644 --- a/vector/v.profile/main.c +++ b/vector/v.profile/main.c @@ -34,7 +34,7 @@ #include #include #include - +#include #include "local_proto.h" #if HAVE_GEOS @@ -46,7 +46,9 @@ * buffers are fixed. (Will happen at some point after next ten years * or so as there's nothing more permanent than a temporary solution.) * 2017-11-19 - */ +*/ +enum OutputFormat { PLAIN, CSV, JSON }; + static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) { int i, ncoords; @@ -77,27 +79,27 @@ static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) G_fatal_error(_("Invalid y coordinate %f"), y); Vect_append_point(Points, x, y, z); } - + return 1; } -/* Helper for converting multipoligons to GRASS polygons */ -static void add_poly(const GEOSGeometry *OGeom, struct line_pnts *Buffer) -{ - const GEOSGeometry *geom2; - static struct line_pnts *gPoints; - int i, nrings; - - gPoints = Vect_new_line_struct(); - - geom2 = GEOSGetExteriorRing(OGeom); - if (!ring2pts(geom2, gPoints)) { - G_fatal_error(_("Corrupt GEOS geometry")); - } - - Vect_append_points(Buffer, gPoints, GV_FORWARD); - Vect_reset_line(gPoints); - + /* Helper for converting multipoligons to GRASS polygons */ + static void add_poly(const GEOSGeometry *OGeom, struct line_pnts *Buffer) + { + const GEOSGeometry *geom2; + static struct line_pnts *gPoints; + int i, nrings; + + gPoints = Vect_new_line_struct(); + + geom2 = GEOSGetExteriorRing(OGeom); + if (!ring2pts(geom2, gPoints)) { + G_fatal_error(_("Corrupt GEOS geometry")); + } + + Vect_append_points(Buffer, gPoints, GV_FORWARD); + Vect_reset_line(gPoints); + nrings = GEOSGetNumInteriorRings(OGeom); for (i = 0; i < nrings; i++) { @@ -136,7 +138,7 @@ int main(int argc, char *argv[]) struct GModule *module; struct Option *old_map, *new_map, *coords_opt, *buffer_opt, *delim_opt, *dp_opt, *layer_opt, *where_opt, *inline_map, *inline_where, - *inline_layer, *type_opt, *file_opt; + *inline_layer, *type_opt, *file_opt, *format_opt; struct Flag *no_column_flag, *no_z_flag; struct field_info *Fi, *Fpro; @@ -147,6 +149,11 @@ int main(int argc, char *argv[]) dbColumn *column; dbString table_name, dbsql, valstr; + enum OutputFormat format; + G_JSON_Value *root_value = NULL, *item_value = NULL; + G_JSON_Array *root_array = NULL; + G_JSON_Object *item_object = NULL; + /* initialize GIS environment */ G_gisinit(argv[0]); @@ -197,6 +204,15 @@ int main(int argc, char *argv[]) dp_opt->description = _("Number of significant digits"); dp_opt->guisection = _("Format"); + format_opt = G_define_standard_option(G_OPT_F_FORMAT); + format_opt->options = "plain,csv,json"; + format_opt->required = NO; + format_opt->answer = "plain"; + format_opt->descriptions = _("plain;Human readable text output;" + "csv;CSV (Comma Separated Values);" + "json;JSON (JavaScript Object Notation)"); + format_opt->guisection = _("Format"); + /* Profiling tolerance */ buffer_opt = G_define_option(); buffer_opt->key = "buffer"; @@ -272,6 +288,21 @@ int main(int argc, char *argv[]) if (G_parser(argc, argv)) exit(EXIT_FAILURE); + + if (strcmp(format_opt->answer, "json") == 0) { + format = JSON; + root_value = G_json_value_init_array(); + if (root_value == NULL) { + G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); + } + root_array = G_json_array(root_value); + } + else if (strcmp(format_opt->answer, "csv") == 0) { + format = CSV; + } + else { + format = PLAIN; + } #if HAVE_GEOS #if (GEOS_VERSION_MAJOR < 3 || \ @@ -630,7 +661,7 @@ int main(int argc, char *argv[]) case GV_POINT: Vect_cat_get(Cats, layer, &cat); proc_point(Points, Profil, Buffer, cat, &rescount, - open3d); + open3d); break; case GV_LINE: Vect_reset_line(Ipoints); @@ -677,84 +708,214 @@ int main(int argc, char *argv[]) /* Sort results by distance, cat */ qsort(&resultset[0], rescount, sizeof(Result), compdist); - /* Print out column names */ - if (!no_column_flag->answer) { - fprintf(ascii, "Number%sDistance", fs); - if (open3d == WITH_Z) - fprintf(ascii, "%sZ", fs); - if (Fi != NULL) { - /* ncols are initialized here from previous Fi != NULL if block */ - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - fprintf(ascii, "%s%s", fs, db_get_column_name(column)); + /* Print output based on format */ + switch (format) { + case PLAIN: + /* Print header for PLAIN format */ + if (!no_column_flag->answer) { + fprintf(ascii, "Number%sDistance", fs); + if (open3d == WITH_Z) + fprintf(ascii, "%sZ", fs); + if (Fi != NULL) { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + fprintf(ascii, "%s%s", fs, db_get_column_name(column)); + } + } + fprintf(ascii, "\n"); } - } - fprintf(ascii, "\n"); - } - - /* Print out result */ - for (j = 0; j < rescount; j++) { - fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); - if (open3d == WITH_Z) - fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); - if (Fi != NULL) { - snprintf(sql, sizeof(sql), "select * from %s where %s=%d", - Fi->table, Fi->key, resultset[j].cat); - G_debug(2, "SQL: \"%s\"", sql); - db_set_string(&dbsql, sql); - /* driver IS initialized here in case if Fi != NULL */ - if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != - DB_OK) - G_warning(_("Unable to get attribute data for cat %d"), - resultset[j].cat); - else { - nrows = db_get_num_rows(&cursor); - G_debug(1, "Result count: %d", nrows); - table = db_get_cursor_table(&cursor); - - if (nrows > 0) { - if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { - G_warning(_("Error while retrieving database record " - "for cat %d"), - resultset[j].cat); + + /* Print rows */ + for (j = 0; j < rescount; j++) { + fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); + if (open3d == WITH_Z) + fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); + + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); + fprintf(ascii, "\n"); } else { - for (col = 0; col < ncols; col++) { - /* Column description retrieving is fast, as they - * live in provided table structure */ - column = db_get_table_column(table, col); - db_convert_column_value_to_string(column, &valstr); - type = db_get_column_sqltype(column); - - /* Those values should be quoted */ - if (type == DB_SQL_TYPE_CHARACTER || - type == DB_SQL_TYPE_DATE || - type == DB_SQL_TYPE_TIME || - type == DB_SQL_TYPE_TIMESTAMP || - type == DB_SQL_TYPE_INTERVAL || - type == DB_SQL_TYPE_TEXT || - type == DB_SQL_TYPE_SERIAL) - fprintf(ascii, "%s\"%s\"", fs, - db_get_string(&valstr)); - else - fprintf(ascii, "%s%s", fs, - db_get_string(&valstr)); + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database record for cat %d"), + resultset[j].cat); + } + else { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + db_convert_column_value_to_string(column, &valstr); + type = db_get_column_sqltype(column); + + if (type == DB_SQL_TYPE_CHARACTER || + type == DB_SQL_TYPE_DATE || + type == DB_SQL_TYPE_TIME || + type == DB_SQL_TYPE_TIMESTAMP || + type == DB_SQL_TYPE_INTERVAL || + type == DB_SQL_TYPE_TEXT || + type == DB_SQL_TYPE_SERIAL) + fprintf(ascii, "%s\"%s\"", fs, db_get_string(&valstr)); + else + fprintf(ascii, "%s%s", fs, db_get_string(&valstr)); + } + } + db_close_cursor(&cursor); } } } - else { + fprintf(ascii, "\n"); + } + break; + + case CSV: + /* CSV header */ + if (!no_column_flag->answer) { + fprintf(ascii, "Number%sDistance", fs); + if (open3d == WITH_Z) + fprintf(ascii, "%sZ", fs); + if (Fi != NULL) { for (col = 0; col < ncols; col++) { - fprintf(ascii, "%s", fs); + column = db_get_table_column(table, col); + fprintf(ascii, "%s%s", fs, db_get_column_name(column)); + } + } + fprintf(ascii, "\n"); + } + + for (j = 0; j < rescount; j++) { + fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); + if (open3d == WITH_Z) + fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); + + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); + fprintf(ascii, "\n"); + } + else { + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database record for cat %d"), + resultset[j].cat); + } + else { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + db_convert_column_value_to_string(column, &valstr); + fprintf(ascii, "%s%s", fs, db_get_string(&valstr)); + } + } + db_close_cursor(&cursor); + } + } + } + fprintf(ascii, "\n"); + } + break; + + case JSON: + /* Build JSON array */ + for (j = 0; j < rescount; j++) { + item_value = G_json_value_init_object(); + if (item_value == NULL) { + G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); + } + item_object = G_json_object(item_value); + + /* Always include category and distance */ + G_json_object_set_number(item_object, "category", resultset[j].cat); + G_json_object_set_number(item_object, "distance", resultset[j].distance); + + if (open3d == WITH_Z) + G_json_object_set_number(item_object, "z", resultset[j].z); + + /* Add attributes */ + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); + fprintf(ascii, "\n"); + } + else { + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database record for cat %d"), + resultset[j].cat); + } + else { + G_JSON_Value *attr_value = G_json_value_init_object(); + G_JSON_Object *attr_object = G_json_object(attr_value); + + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + dbValue *value = db_get_column_value(column); + int ctype = db_get_column_sqltype(column); + const char *col_name = db_get_column_name(column); + + if (db_test_value_isnull(value)) { + G_json_object_set_null(attr_object, col_name); + } + else if (ctype == DB_SQL_TYPE_INTEGER) { + G_json_object_set_number(attr_object, col_name, + db_get_value_int(value)); + } + else if (ctype == DB_SQL_TYPE_REAL) { + G_json_object_set_number(attr_object, col_name, + db_get_value_double(value)); + } + else { + db_convert_column_value_to_string(column, &valstr); + G_json_object_set_string(attr_object, col_name, + db_get_string(&valstr)); + } + } + + G_json_object_set_value(item_object, "attributes", attr_value); + } + db_close_cursor(&cursor); + } } + } + G_json_array_append_value(root_array, item_value); } - db_close_cursor(&cursor); + /* Output JSON */ + char *json_string = G_json_serialize_to_string_pretty(root_value); + if (!json_string) { + G_json_value_free(root_value); + G_fatal_error(_("Failed to serialize JSON to pretty format.")); } - /* Terminate attribute data line and flush data to provided output - * (file/stdout) */ - fprintf(ascii, "\n"); - if (fflush(ascii)) - G_fatal_error(_("Can not write data portion to provided output")); + + fputs(json_string, ascii); + fputc('\n', ascii); + + G_json_free_serialized_string(json_string); + G_json_value_free(root_value); + break; } /* Build topology for vector map and close them */ From a8597a058f5a839ae4357b6e45c86307fff73526 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Mon, 5 Jan 2026 21:02:22 +0530 Subject: [PATCH 2/9] v.profile: Added testJsonFormat() for JSON validation --- vector/v.profile/testsuite/test_v_profile.py | 45 +++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/vector/v.profile/testsuite/test_v_profile.py b/vector/v.profile/testsuite/test_v_profile.py index d7f882492e2..8386c75a284 100644 --- a/vector/v.profile/testsuite/test_v_profile.py +++ b/vector/v.profile/testsuite/test_v_profile.py @@ -11,7 +11,7 @@ TODO: Convert to synthetic dataset. It would allow to shorten output sample length. Cover more input/output combinations. """ - +import json from pathlib import Path from grass.gunittest.case import TestCase @@ -233,6 +233,49 @@ def testBuffering(self): ) vpro.run() + def testJsonFormat(self): + """Test JSON format output""" + vpro = SimpleModule( + "v.profile", + input=self.in_points, + profile_map=self.in_map, + buffer=200, + profile_where=self.where, + format="json" + ) + vpro.run() + + actual = json.loads(vpro.outputs.stdout) + + expected = [ + { + "category": 572, + "distance": 19537.97, + "attributes": { + "featurenam": "Greshams Lake", + "class": "Reservoir", + } + }, + { + "category": 1029, + "distance": 19537.97, + "attributes": { + "featurenam": "Greshams Lake Dam", + "class": "Dam", + } + } + ] + + # Compare key fields + self.assertEqual(len(actual), len(expected)) + for i in range(len(expected)): + self.assertEqual(actual[i]["category"], expected[i]["category"]) + self.assertAlmostEqual(actual[i]["distance"], expected[i]["distance"], places=2) + self.assertEqual(actual[i]["attributes"]["featurenam"], + expected[i]["attributes"]["featurenam"]) + self.assertEqual(actual[i]["attributes"]["class"], + expected[i]["attributes"]["class"]) + def testMultiCrossing(self): """If profile crosses single line multiple times, all crossings should be reported""" From 122874bd11f88931a98340f74463056a782d59f5 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Mon, 5 Jan 2026 21:02:56 +0530 Subject: [PATCH 3/9] v.profile: Updated documentation for new output formats --- vector/v.profile/v.profile.md | 61 +++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/vector/v.profile/v.profile.md b/vector/v.profile/v.profile.md index 3471fdc9578..5c346d1b7c1 100644 --- a/vector/v.profile/v.profile.md +++ b/vector/v.profile/v.profile.md @@ -1,8 +1,8 @@ ## DESCRIPTION *v.profile* prints out distance and attributes of points/lines along a -profiling line. Distance is calculated from the first profiling line -coordinate pair or from the beginning of vector line. +profiling line. Distance is calculated from the first profiling line +coordinate pair or from the beginning of vector line. The *buffer* (tolerance) parameter sets how far point can be located from a profiling line and still be included in the output data set. The *output* map option can be used to visually check which points are @@ -29,6 +29,63 @@ depends on GEOS and will not function if GRASS is compiled without GEOS. This restriction will be removed as soon as GRASS native buffer generation is fixed. +## OUTPUT FORMATS + +The module supports three output formats specified via the `format` parameter. +If `format=` is not specified, plain format is used by default. + +### Plain format (default) + +Traditional CSV output using the delimiter (default pipe `|`). +String values are enclosed in double quotes for backward compatibility with existing +scripts. This is the default format and does not require explicitly setting `format=plain`. + +Example output: +```text +Number|Distance|cat|ID|LABEL +1|2750.44|60|42|"Morrisville #2" +``` + +### CSV format + +Delimiter-separated output without quotes around string values, suitable for +spreadsheet applications and data processing tools. Uses the same *separator* +as plain format. + +For true comma-separated values, combine with `separator=comma`: +`v.profile input=data buffer=100 profile_map=line format=csv separator=comma` + + +Example output with comma separator: +```text +Number,Distance,cat,ID,LABEL +1,2750.44,60,42,Morrisville #2 +``` + + +### JSON format + +Structured JSON output with nested attributes. Each feature is represented +as an object containing `category`, `distance`, and `attributes` fields. + +Example output: +```text +[ + { + "category": 60, + "distance": 2750.44, + "attributes": { + "cat": 60, + "ID": 42, + "LABEL": "Morrisville #2" + } + } +] +``` + +Note: In JSON format, the category is always included even when no attribute +table is linked. + ## EXAMPLES List all geonames along part of road NC-96 (NC Basic dataset). The From d27a5c11b743dabd406d0cbe355f7d0ecf26c243 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Wed, 7 Jan 2026 15:59:13 +0530 Subject: [PATCH 4/9] v.profile: Added CSV and JSON output formats --- vector/CMakeLists.txt | 1 + vector/v.profile/main.c | 415 +++++++++++++++++++++------------------- 2 files changed, 221 insertions(+), 195 deletions(-) diff --git a/vector/CMakeLists.txt b/vector/CMakeLists.txt index 72397f95a61..d7b4f5eb587 100644 --- a/vector/CMakeLists.txt +++ b/vector/CMakeLists.txt @@ -391,6 +391,7 @@ build_program_in_subdir( grass_dbmiclient grass_gis grass_vector + grass_parson OPTIONAL_DEPENDS GEOS::geos_c) diff --git a/vector/v.profile/main.c b/vector/v.profile/main.c index b120b80afd5..5cc5d0a6dd4 100644 --- a/vector/v.profile/main.c +++ b/vector/v.profile/main.c @@ -46,7 +46,7 @@ * buffers are fixed. (Will happen at some point after next ten years * or so as there's nothing more permanent than a temporary solution.) * 2017-11-19 -*/ + */ enum OutputFormat { PLAIN, CSV, JSON }; static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) @@ -79,27 +79,27 @@ static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) G_fatal_error(_("Invalid y coordinate %f"), y); Vect_append_point(Points, x, y, z); } - + return 1; } - /* Helper for converting multipoligons to GRASS polygons */ - static void add_poly(const GEOSGeometry *OGeom, struct line_pnts *Buffer) - { - const GEOSGeometry *geom2; - static struct line_pnts *gPoints; - int i, nrings; - - gPoints = Vect_new_line_struct(); - - geom2 = GEOSGetExteriorRing(OGeom); - if (!ring2pts(geom2, gPoints)) { - G_fatal_error(_("Corrupt GEOS geometry")); - } - - Vect_append_points(Buffer, gPoints, GV_FORWARD); - Vect_reset_line(gPoints); - +/* Helper for converting multipoligons to GRASS polygons */ +static void add_poly(const GEOSGeometry *OGeom, struct line_pnts *Buffer) +{ + const GEOSGeometry *geom2; + static struct line_pnts *gPoints; + int i, nrings; + + gPoints = Vect_new_line_struct(); + + geom2 = GEOSGetExteriorRing(OGeom); + if (!ring2pts(geom2, gPoints)) { + G_fatal_error(_("Corrupt GEOS geometry")); + } + + Vect_append_points(Buffer, gPoints, GV_FORWARD); + Vect_reset_line(gPoints); + nrings = GEOSGetNumInteriorRings(OGeom); for (i = 0; i < nrings; i++) { @@ -209,8 +209,8 @@ int main(int argc, char *argv[]) format_opt->required = NO; format_opt->answer = "plain"; format_opt->descriptions = _("plain;Human readable text output;" - "csv;CSV (Comma Separated Values);" - "json;JSON (JavaScript Object Notation)"); + "csv;CSV (Comma Separated Values);" + "json;JSON (JavaScript Object Notation)"); format_opt->guisection = _("Format"); /* Profiling tolerance */ @@ -288,7 +288,6 @@ int main(int argc, char *argv[]) if (G_parser(argc, argv)) exit(EXIT_FAILURE); - if (strcmp(format_opt->answer, "json") == 0) { format = JSON; root_value = G_json_value_init_array(); @@ -420,6 +419,7 @@ int main(int argc, char *argv[]) /* the field separator */ fs = G_option_to_separator(delim_opt); + const char *csv_separator = ","; /* Let's get vector layers db connections information */ Fi = Vect_get_field(&In, layer); @@ -661,7 +661,7 @@ int main(int argc, char *argv[]) case GV_POINT: Vect_cat_get(Cats, layer, &cat); proc_point(Points, Profil, Buffer, cat, &rescount, - open3d); + open3d); break; case GV_LINE: Vect_reset_line(Ipoints); @@ -710,209 +710,234 @@ int main(int argc, char *argv[]) /* Print output based on format */ switch (format) { - case PLAIN: - /* Print header for PLAIN format */ - if (!no_column_flag->answer) { - fprintf(ascii, "Number%sDistance", fs); - if (open3d == WITH_Z) - fprintf(ascii, "%sZ", fs); - if (Fi != NULL) { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - fprintf(ascii, "%s%s", fs, db_get_column_name(column)); - } + case PLAIN: + /* Print header for PLAIN format */ + if (!no_column_flag->answer) { + fprintf(ascii, "Number%sDistance", fs); + if (open3d == WITH_Z) + fprintf(ascii, "%sZ", fs); + if (Fi != NULL) { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + fprintf(ascii, "%s%s", fs, db_get_column_name(column)); } - fprintf(ascii, "\n"); } - - /* Print rows */ - for (j = 0; j < rescount; j++) { - fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); - if (open3d == WITH_Z) - fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); - - if (Fi != NULL) { - snprintf(sql, sizeof(sql), "select * from %s where %s=%d", - Fi->table, Fi->key, resultset[j].cat); - G_debug(2, "SQL: \"%s\"", sql); - db_set_string(&dbsql, sql); - - if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { - G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); - fprintf(ascii, "\n"); - } - else { - nrows = db_get_num_rows(&cursor); - table = db_get_cursor_table(&cursor); - - if (nrows > 0) { - if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { - G_warning(_("Error while retrieving database record for cat %d"), - resultset[j].cat); - } - else { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - db_convert_column_value_to_string(column, &valstr); - type = db_get_column_sqltype(column); - - if (type == DB_SQL_TYPE_CHARACTER || - type == DB_SQL_TYPE_DATE || - type == DB_SQL_TYPE_TIME || - type == DB_SQL_TYPE_TIMESTAMP || - type == DB_SQL_TYPE_INTERVAL || - type == DB_SQL_TYPE_TEXT || - type == DB_SQL_TYPE_SERIAL) - fprintf(ascii, "%s\"%s\"", fs, db_get_string(&valstr)); - else - fprintf(ascii, "%s%s", fs, db_get_string(&valstr)); - } + fprintf(ascii, "\n"); + } + + /* Print rows */ + for (j = 0; j < rescount; j++) { + fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); + if (open3d == WITH_Z) + fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); + + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, + DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), + resultset[j].cat); + fprintf(ascii, "\n"); + } + else { + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database " + "record for cat %d"), + resultset[j].cat); + } + else { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + db_convert_column_value_to_string(column, + &valstr); + type = db_get_column_sqltype(column); + + if (type == DB_SQL_TYPE_CHARACTER || + type == DB_SQL_TYPE_DATE || + type == DB_SQL_TYPE_TIME || + type == DB_SQL_TYPE_TIMESTAMP || + type == DB_SQL_TYPE_INTERVAL || + type == DB_SQL_TYPE_TEXT || + type == DB_SQL_TYPE_SERIAL) + fprintf(ascii, "%s\"%s\"", fs, + db_get_string(&valstr)); + else + fprintf(ascii, "%s%s", fs, + db_get_string(&valstr)); } - db_close_cursor(&cursor); } + db_close_cursor(&cursor); } } - fprintf(ascii, "\n"); - } + } + fprintf(ascii, "\n"); + } break; - - case CSV: - /* CSV header */ - if (!no_column_flag->answer) { - fprintf(ascii, "Number%sDistance", fs); - if (open3d == WITH_Z) - fprintf(ascii, "%sZ", fs); - if (Fi != NULL) { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - fprintf(ascii, "%s%s", fs, db_get_column_name(column)); - } + + case CSV: + /* CSV header */ + if (!no_column_flag->answer) { + fprintf(ascii, "Number%sDistance", csv_separator); + if (open3d == WITH_Z) + fprintf(ascii, "%sZ", csv_separator); + if (Fi != NULL) { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + fprintf(ascii, "%s%s", csv_separator, + db_get_column_name(column)); } - fprintf(ascii, "\n"); } - - for (j = 0; j < rescount; j++) { - fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); - if (open3d == WITH_Z) - fprintf(ascii, "%s%.*f", fs, dp, resultset[j].z); - - if (Fi != NULL) { - snprintf(sql, sizeof(sql), "select * from %s where %s=%d", - Fi->table, Fi->key, resultset[j].cat); - G_debug(2, "SQL: \"%s\"", sql); - db_set_string(&dbsql, sql); - - if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { - G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); - fprintf(ascii, "\n"); - } - else { - nrows = db_get_num_rows(&cursor); - table = db_get_cursor_table(&cursor); - - if (nrows > 0) { - if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { - G_warning(_("Error while retrieving database record for cat %d"), - resultset[j].cat); - } - else { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - db_convert_column_value_to_string(column, &valstr); - fprintf(ascii, "%s%s", fs, db_get_string(&valstr)); - } + fprintf(ascii, "\n"); + } + + for (j = 0; j < rescount; j++) { + fprintf(ascii, "%zu%s%.*f", j + 1, csv_separator, dp, + resultset[j].distance); + if (open3d == WITH_Z) + fprintf(ascii, "%s%.*f", csv_separator, dp, resultset[j].z); + + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, + DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), + resultset[j].cat); + fprintf(ascii, "\n"); + } + else { + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database " + "record for cat %d"), + resultset[j].cat); + } + else { + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + db_convert_column_value_to_string(column, + &valstr); + fprintf(ascii, "%s%s", csv_separator, + db_get_string(&valstr)); } - db_close_cursor(&cursor); } + db_close_cursor(&cursor); } } - fprintf(ascii, "\n"); } + fprintf(ascii, "\n"); + } break; - case JSON: - /* Build JSON array */ - for (j = 0; j < rescount; j++) { - item_value = G_json_value_init_object(); - if (item_value == NULL) { - G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); + case JSON: + /* Build JSON array */ + for (j = 0; j < rescount; j++) { + item_value = G_json_value_init_object(); + if (item_value == NULL) { + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + item_object = G_json_object(item_value); + + /* Always include category and distance */ + G_json_object_set_number(item_object, "category", resultset[j].cat); + G_json_object_set_number(item_object, "distance", + resultset[j].distance); + + if (open3d == WITH_Z) + G_json_object_set_number(item_object, "z", resultset[j].z); + + /* Add attributes */ + if (Fi != NULL) { + snprintf(sql, sizeof(sql), "select * from %s where %s=%d", + Fi->table, Fi->key, resultset[j].cat); + G_debug(2, "SQL: \"%s\"", sql); + db_set_string(&dbsql, sql); + + if (db_open_select_cursor(driver, &dbsql, &cursor, + DB_SEQUENTIAL) != DB_OK) { + G_warning(_("Unable to get attribute data for cat %d"), + resultset[j].cat); } - item_object = G_json_object(item_value); - - /* Always include category and distance */ - G_json_object_set_number(item_object, "category", resultset[j].cat); - G_json_object_set_number(item_object, "distance", resultset[j].distance); - - if (open3d == WITH_Z) - G_json_object_set_number(item_object, "z", resultset[j].z); - - /* Add attributes */ - if (Fi != NULL) { - snprintf(sql, sizeof(sql), "select * from %s where %s=%d", - Fi->table, Fi->key, resultset[j].cat); - G_debug(2, "SQL: \"%s\"", sql); - db_set_string(&dbsql, sql); - - if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { - G_warning(_("Unable to get attribute data for cat %d"), resultset[j].cat); - fprintf(ascii, "\n"); - } - else { - nrows = db_get_num_rows(&cursor); - table = db_get_cursor_table(&cursor); - - if (nrows > 0) { - if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { - G_warning(_("Error while retrieving database record for cat %d"), - resultset[j].cat); - } - else { - G_JSON_Value *attr_value = G_json_value_init_object(); - G_JSON_Object *attr_object = G_json_object(attr_value); - - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - dbValue *value = db_get_column_value(column); - int ctype = db_get_column_sqltype(column); - const char *col_name = db_get_column_name(column); - - if (db_test_value_isnull(value)) { - G_json_object_set_null(attr_object, col_name); - } - else if (ctype == DB_SQL_TYPE_INTEGER) { - G_json_object_set_number(attr_object, col_name, - db_get_value_int(value)); - } - else if (ctype == DB_SQL_TYPE_REAL) { - G_json_object_set_number(attr_object, col_name, - db_get_value_double(value)); - } - else { - db_convert_column_value_to_string(column, &valstr); - G_json_object_set_string(attr_object, col_name, - db_get_string(&valstr)); - } + else { + nrows = db_get_num_rows(&cursor); + table = db_get_cursor_table(&cursor); + + if (nrows > 0) { + if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { + G_warning(_("Error while retrieving database " + "record for cat %d"), + resultset[j].cat); + } + else { + G_JSON_Value *attr_value = + G_json_value_init_object(); + G_JSON_Object *attr_object = + G_json_object(attr_value); + + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + dbValue *value = db_get_column_value(column); + int ctype = db_get_column_sqltype(column); + const char *col_name = + db_get_column_name(column); + + if (db_test_value_isnull(value)) { + G_json_object_set_null(attr_object, + col_name); + } + else if (ctype == DB_SQL_TYPE_INTEGER) { + G_json_object_set_number( + attr_object, col_name, + db_get_value_int(value)); + } + else if (ctype == DB_SQL_TYPE_REAL) { + G_json_object_set_number( + attr_object, col_name, + db_get_value_double(value)); + } + else { + db_convert_column_value_to_string(column, + &valstr); + G_json_object_set_string( + attr_object, col_name, + db_get_string(&valstr)); } - - G_json_object_set_value(item_object, "attributes", attr_value); } - db_close_cursor(&cursor); + + G_json_object_set_value(item_object, "attributes", + attr_value); } + db_close_cursor(&cursor); } - } - G_json_array_append_value(root_array, item_value); } + G_json_array_append_value(root_array, item_value); + } /* Output JSON */ char *json_string = G_json_serialize_to_string_pretty(root_value); if (!json_string) { G_json_value_free(root_value); G_fatal_error(_("Failed to serialize JSON to pretty format.")); } - + fputs(json_string, ascii); fputc('\n', ascii); - + G_json_free_serialized_string(json_string); G_json_value_free(root_value); break; From 48c2738d835fd0f3e36063bf0ffb542853c9c2ac Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Wed, 7 Jan 2026 16:01:27 +0530 Subject: [PATCH 5/9] Added tests for JSON format output --- vector/v.profile/testsuite/test_v_profile.py | 30 ++++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/vector/v.profile/testsuite/test_v_profile.py b/vector/v.profile/testsuite/test_v_profile.py index 8386c75a284..82d70204f86 100644 --- a/vector/v.profile/testsuite/test_v_profile.py +++ b/vector/v.profile/testsuite/test_v_profile.py @@ -11,6 +11,7 @@ TODO: Convert to synthetic dataset. It would allow to shorten output sample length. Cover more input/output combinations. """ + import json from pathlib import Path @@ -241,12 +242,12 @@ def testJsonFormat(self): profile_map=self.in_map, buffer=200, profile_where=self.where, - format="json" + format="json", ) vpro.run() - + actual = json.loads(vpro.outputs.stdout) - + expected = [ { "category": 572, @@ -254,7 +255,7 @@ def testJsonFormat(self): "attributes": { "featurenam": "Greshams Lake", "class": "Reservoir", - } + }, }, { "category": 1029, @@ -262,19 +263,24 @@ def testJsonFormat(self): "attributes": { "featurenam": "Greshams Lake Dam", "class": "Dam", - } - } + }, + }, ] - + # Compare key fields self.assertEqual(len(actual), len(expected)) for i in range(len(expected)): self.assertEqual(actual[i]["category"], expected[i]["category"]) - self.assertAlmostEqual(actual[i]["distance"], expected[i]["distance"], places=2) - self.assertEqual(actual[i]["attributes"]["featurenam"], - expected[i]["attributes"]["featurenam"]) - self.assertEqual(actual[i]["attributes"]["class"], - expected[i]["attributes"]["class"]) + self.assertAlmostEqual( + actual[i]["distance"], expected[i]["distance"], places=2 + ) + self.assertEqual( + actual[i]["attributes"]["featurenam"], + expected[i]["attributes"]["featurenam"], + ) + self.assertEqual( + actual[i]["attributes"]["class"], expected[i]["attributes"]["class"] + ) def testMultiCrossing(self): """If profile crosses single line multiple times, all crossings From ec956be7b1347990a9a0c84e6e9af3034374ac03 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Wed, 7 Jan 2026 16:09:15 +0530 Subject: [PATCH 6/9] Update documentation for output formats --- vector/v.profile/v.profile.md | 146 ++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 58 deletions(-) diff --git a/vector/v.profile/v.profile.md b/vector/v.profile/v.profile.md index 5c346d1b7c1..0f43302cf36 100644 --- a/vector/v.profile/v.profile.md +++ b/vector/v.profile/v.profile.md @@ -1,7 +1,7 @@ ## DESCRIPTION *v.profile* prints out distance and attributes of points/lines along a -profiling line. Distance is calculated from the first profiling line +profiling line. Distance is calculated from the first profiling line coordinate pair or from the beginning of vector line. The *buffer* (tolerance) parameter sets how far point can be located from a profiling line and still be included in the output data set. @@ -29,63 +29,6 @@ depends on GEOS and will not function if GRASS is compiled without GEOS. This restriction will be removed as soon as GRASS native buffer generation is fixed. -## OUTPUT FORMATS - -The module supports three output formats specified via the `format` parameter. -If `format=` is not specified, plain format is used by default. - -### Plain format (default) - -Traditional CSV output using the delimiter (default pipe `|`). -String values are enclosed in double quotes for backward compatibility with existing -scripts. This is the default format and does not require explicitly setting `format=plain`. - -Example output: -```text -Number|Distance|cat|ID|LABEL -1|2750.44|60|42|"Morrisville #2" -``` - -### CSV format - -Delimiter-separated output without quotes around string values, suitable for -spreadsheet applications and data processing tools. Uses the same *separator* -as plain format. - -For true comma-separated values, combine with `separator=comma`: -`v.profile input=data buffer=100 profile_map=line format=csv separator=comma` - - -Example output with comma separator: -```text -Number,Distance,cat,ID,LABEL -1,2750.44,60,42,Morrisville #2 -``` - - -### JSON format - -Structured JSON output with nested attributes. Each feature is represented -as an object containing `category`, `distance`, and `attributes` fields. - -Example output: -```text -[ - { - "category": 60, - "distance": 2750.44, - "attributes": { - "cat": 60, - "ID": 42, - "LABEL": "Morrisville #2" - } - } -] -``` - -Note: In JSON format, the category is always included even when no attribute -table is linked. - ## EXAMPLES List all geonames along part of road NC-96 (NC Basic dataset). The @@ -122,6 +65,93 @@ v.profile input=streams@PERMANENT output=/home/user/river_profile.csv \ east_north=600570.27364,4920613.41838,600348.034348,4920840.38617 ``` +### CSV Output + +```sh +v.profile input=firestations@PERMANENT buffer=100 \ + profile_map=railroads@PERMANENT profile_where="cat=3202" \ + format=csv +``` + +The output looks as follows: + +```text +Number,Distance,cat,ID,LABEL,LOCATION,CITY,MUN_COUNT,PUMPERS,PUMPER_TAN,TANKER,MINI_PUMPE,RESCUE_SER,AERIAL,BRUSH,OTHERS,WATER_RESC,MUNCOID,BLDGCODE,AGENCY,STATIONID,RECNO,CV_SID2,CVLAG +1,2750.44,60,42,Morrisville #2,10632 Chapel Hill Rd,Morrisville,M,0,1,0,0,0,1,0,1,0,1,298,FD,MF2A,62,MF2A,1.4 +2,5394.27,2,23,Morrisville #1,100 Morrisville-Carpenter Rd,Morrisville,M,0,1,0,0,1,0,1,3,0,1,241,FD,MF1A,2,MF1A,1.4 +``` + +### JSON Output + +```sh +v.profile input=firestations@PERMANENT buffer=100 \ + profile_map=railroads@PERMANENT profile_where="cat=3202" \ + format=json +``` + +The output looks as follows: + +```json +[ + { + "category": 60, + "distance": 2750.4354923389592, + "attributes": { + "cat": 60, + "ID": 42, + "LABEL": "Morrisville #2", + "LOCATION": "10632 Chapel Hill Rd", + "CITY": "Morrisville", + "MUN_COUNT": "M", + "PUMPERS": 0, + "PUMPER_TAN": 1, + "TANKER": 0, + "MINI_PUMPE": "0", + "RESCUE_SER": 0, + "AERIAL": 1, + "BRUSH": 0, + "OTHERS": 1, + "WATER_RESC": 0, + "MUNCOID": 1, + "BLDGCODE": "298", + "AGENCY": "FD", + "STATIONID": "MF2A", + "RECNO": "62", + "CV_SID2": "MF2A", + "CVLAG": "1.4" + } + }, + { + "category": 2, + "distance": 5394.2668921394143, + "attributes": { + "cat": 2, + "ID": 23, + "LABEL": "Morrisville #1", + "LOCATION": "100 Morrisville-Carpenter Rd", + "CITY": "Morrisville", + "MUN_COUNT": "M", + "PUMPERS": 0, + "PUMPER_TAN": 1, + "TANKER": 0, + "MINI_PUMPE": "0", + "RESCUE_SER": 1, + "AERIAL": 0, + "BRUSH": 1, + "OTHERS": 3, + "WATER_RESC": 0, + "MUNCOID": 1, + "BLDGCODE": "241", + "AGENCY": "FD", + "STATIONID": "MF1A", + "RECNO": "2", + "CV_SID2": "MF1A", + "CVLAG": "1.4" + } + } +] +``` + ## BUGS Strings are enclosed in double quotes ", still quotes within string are From 81b1118297fbbcc51296e314b1b58ea2631336c5 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Wed, 14 Jan 2026 22:38:59 +0530 Subject: [PATCH 7/9] JSON support --- vector/v.profile/main.c | 98 ++++++++--------------------------- vector/v.profile/v.profile.md | 14 ++--- 2 files changed, 30 insertions(+), 82 deletions(-) diff --git a/vector/v.profile/main.c b/vector/v.profile/main.c index 5cc5d0a6dd4..3fbb4b10938 100644 --- a/vector/v.profile/main.c +++ b/vector/v.profile/main.c @@ -47,7 +47,7 @@ * or so as there's nothing more permanent than a temporary solution.) * 2017-11-19 */ -enum OutputFormat { PLAIN, CSV, JSON }; +enum OutputFormat { CSV, JSON }; static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) { @@ -205,11 +205,10 @@ int main(int argc, char *argv[]) dp_opt->guisection = _("Format"); format_opt = G_define_standard_option(G_OPT_F_FORMAT); - format_opt->options = "plain,csv,json"; + format_opt->options = "csv,json"; format_opt->required = NO; - format_opt->answer = "plain"; - format_opt->descriptions = _("plain;Human readable text output;" - "csv;CSV (Comma Separated Values);" + format_opt->answer = "csv"; + format_opt->descriptions = _("csv;CSV (Comma Separated Values);" "json;JSON (JavaScript Object Notation)"); format_opt->guisection = _("Format"); @@ -296,11 +295,8 @@ int main(int argc, char *argv[]) } root_array = G_json_array(root_value); } - else if (strcmp(format_opt->answer, "csv") == 0) { - format = CSV; - } else { - format = PLAIN; + format = CSV; } #if HAVE_GEOS @@ -419,7 +415,6 @@ int main(int argc, char *argv[]) /* the field separator */ fs = G_option_to_separator(delim_opt); - const char *csv_separator = ","; /* Let's get vector layers db connections information */ Fi = Vect_get_field(&In, layer); @@ -710,13 +705,15 @@ int main(int argc, char *argv[]) /* Print output based on format */ switch (format) { - case PLAIN: - /* Print header for PLAIN format */ + case CSV: + /* Print out column names */ if (!no_column_flag->answer) { fprintf(ascii, "Number%sDistance", fs); if (open3d == WITH_Z) fprintf(ascii, "%sZ", fs); if (Fi != NULL) { + /* ncols are initialized here from previous Fi != NULL if block + */ for (col = 0; col < ncols; col++) { column = db_get_table_column(table, col); fprintf(ascii, "%s%s", fs, db_get_column_name(column)); @@ -725,7 +722,7 @@ int main(int argc, char *argv[]) fprintf(ascii, "\n"); } - /* Print rows */ + /* Print out result */ for (j = 0; j < rescount; j++) { fprintf(ascii, "%zu%s%.*f", j + 1, fs, dp, resultset[j].distance); if (open3d == WITH_Z) @@ -736,7 +733,7 @@ int main(int argc, char *argv[]) Fi->table, Fi->key, resultset[j].cat); G_debug(2, "SQL: \"%s\"", sql); db_set_string(&dbsql, sql); - + /* driver IS initialized here in case if Fi != NULL */ if (db_open_select_cursor(driver, &dbsql, &cursor, DB_SEQUENTIAL) != DB_OK) { G_warning(_("Unable to get attribute data for cat %d"), @@ -755,11 +752,14 @@ int main(int argc, char *argv[]) } else { for (col = 0; col < ncols; col++) { + /* Column description retrieving is fast, as + * they live in provided table structure */ column = db_get_table_column(table, col); db_convert_column_value_to_string(column, &valstr); type = db_get_column_sqltype(column); + /* Those values should be quoted */ if (type == DB_SQL_TYPE_CHARACTER || type == DB_SQL_TYPE_DATE || type == DB_SQL_TYPE_TIME || @@ -779,67 +779,11 @@ int main(int argc, char *argv[]) } } fprintf(ascii, "\n"); - } - break; - - case CSV: - /* CSV header */ - if (!no_column_flag->answer) { - fprintf(ascii, "Number%sDistance", csv_separator); - if (open3d == WITH_Z) - fprintf(ascii, "%sZ", csv_separator); - if (Fi != NULL) { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - fprintf(ascii, "%s%s", csv_separator, - db_get_column_name(column)); - } - } - fprintf(ascii, "\n"); - } - - for (j = 0; j < rescount; j++) { - fprintf(ascii, "%zu%s%.*f", j + 1, csv_separator, dp, - resultset[j].distance); - if (open3d == WITH_Z) - fprintf(ascii, "%s%.*f", csv_separator, dp, resultset[j].z); - - if (Fi != NULL) { - snprintf(sql, sizeof(sql), "select * from %s where %s=%d", - Fi->table, Fi->key, resultset[j].cat); - G_debug(2, "SQL: \"%s\"", sql); - db_set_string(&dbsql, sql); - - if (db_open_select_cursor(driver, &dbsql, &cursor, - DB_SEQUENTIAL) != DB_OK) { - G_warning(_("Unable to get attribute data for cat %d"), - resultset[j].cat); - fprintf(ascii, "\n"); - } - else { - nrows = db_get_num_rows(&cursor); - table = db_get_cursor_table(&cursor); - - if (nrows > 0) { - if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) { - G_warning(_("Error while retrieving database " - "record for cat %d"), - resultset[j].cat); - } - else { - for (col = 0; col < ncols; col++) { - column = db_get_table_column(table, col); - db_convert_column_value_to_string(column, - &valstr); - fprintf(ascii, "%s%s", csv_separator, - db_get_string(&valstr)); - } - } - db_close_cursor(&cursor); - } - } - } - fprintf(ascii, "\n"); + /* Terminate attribute data line and flush data to provided output + * (file/stdout) */ + if (fflush(ascii)) + G_fatal_error( + _("Can not write data portion to provided output")); } break; @@ -937,7 +881,9 @@ int main(int argc, char *argv[]) fputs(json_string, ascii); fputc('\n', ascii); - + /* Flush data to provided output (file/stdout) */ + if (fflush(ascii)) + G_fatal_error(_("Can not write data portion to provided output")); G_json_free_serialized_string(json_string); G_json_value_free(root_value); break; diff --git a/vector/v.profile/v.profile.md b/vector/v.profile/v.profile.md index 0f43302cf36..c871cfc34a2 100644 --- a/vector/v.profile/v.profile.md +++ b/vector/v.profile/v.profile.md @@ -65,22 +65,24 @@ v.profile input=streams@PERMANENT output=/home/user/river_profile.csv \ east_north=600570.27364,4920613.41838,600348.034348,4920840.38617 ``` -### CSV Output +### Default Output ```sh v.profile input=firestations@PERMANENT buffer=100 \ - profile_map=railroads@PERMANENT profile_where="cat=3202" \ - format=csv + profile_map=railroads@PERMANENT profile_where="cat=3202" ``` The output looks as follows: ```text -Number,Distance,cat,ID,LABEL,LOCATION,CITY,MUN_COUNT,PUMPERS,PUMPER_TAN,TANKER,MINI_PUMPE,RESCUE_SER,AERIAL,BRUSH,OTHERS,WATER_RESC,MUNCOID,BLDGCODE,AGENCY,STATIONID,RECNO,CV_SID2,CVLAG -1,2750.44,60,42,Morrisville #2,10632 Chapel Hill Rd,Morrisville,M,0,1,0,0,0,1,0,1,0,1,298,FD,MF2A,62,MF2A,1.4 -2,5394.27,2,23,Morrisville #1,100 Morrisville-Carpenter Rd,Morrisville,M,0,1,0,0,1,0,1,3,0,1,241,FD,MF1A,2,MF1A,1.4 +Number|Distance|cat|ID|LABEL|LOCATION|CITY|MUN_COUNT|PUMPERS|PUMPER_TAN|TANKER|MINI_PUMPE|RESCUE_SER|AERIAL|BRUSH|OTHERS|WATER_RESC|MUNCOID|BLDGCODE|AGENCY|STATIONID|RECNO|CV_SID2|CVLAG +1|2750.44|60|42|"Morrisville #2"|"10632 Chapel Hill Rd"|"Morrisville"|"M"|0|1|0|0|0|1|0|1|0|1|"298"|"FD"|"MF2A"|62|"MF2A"|1.4 +2|5394.27|2|23|"Morrisville #1"|"100 Morrisville-Carpenter Rd"|"Morrisville"|"M"|0|1|0|0|1|0|1|3|0|1|"241"|"FD"|"MF1A"|2|"MF1A"|1.4 ``` +**Note:** You can use different delimiters with the `separator=` option +(e.g., `separator=comma`, `separator=tab`, `separator=space`). + ### JSON Output ```sh From d06bd830fa47cf88e30a5dc7d186b03f3f2b2548 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Sun, 18 Jan 2026 20:56:50 +0530 Subject: [PATCH 8/9] Add output formats --- vector/v.profile/main.c | 19 ++++++++++++++----- vector/v.profile/v.profile.md | 11 ++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/vector/v.profile/main.c b/vector/v.profile/main.c index 3fbb4b10938..a54b0f3cf03 100644 --- a/vector/v.profile/main.c +++ b/vector/v.profile/main.c @@ -47,7 +47,7 @@ * or so as there's nothing more permanent than a temporary solution.) * 2017-11-19 */ -enum OutputFormat { CSV, JSON }; +enum OutputFormat { PLAIN, CSV, JSON }; static int ring2pts(const GEOSGeometry *geom, struct line_pnts *Points) { @@ -205,10 +205,11 @@ int main(int argc, char *argv[]) dp_opt->guisection = _("Format"); format_opt = G_define_standard_option(G_OPT_F_FORMAT); - format_opt->options = "csv,json"; + format_opt->options = "plain,csv,json"; format_opt->required = NO; - format_opt->answer = "csv"; - format_opt->descriptions = _("csv;CSV (Comma Separated Values);" + format_opt->answer = "plain"; + format_opt->descriptions = _("plain;Plain text with pipe separator;" + "csv;CSV (Comma Separated Values);" "json;JSON (JavaScript Object Notation)"); format_opt->guisection = _("Format"); @@ -295,9 +296,12 @@ int main(int argc, char *argv[]) } root_array = G_json_array(root_value); } - else { + else if (strcmp(format_opt->answer, "csv") == 0) { format = CSV; } + else { + format = PLAIN; + } #if HAVE_GEOS #if (GEOS_VERSION_MAJOR < 3 || \ @@ -414,6 +418,10 @@ int main(int argc, char *argv[]) open3d = WITHOUT_Z; /* the field separator */ + if (delim_opt->count == 0 && format == CSV) { + delim_opt->answer = "comma"; + } + fs = G_option_to_separator(delim_opt); /* Let's get vector layers db connections information */ @@ -705,6 +713,7 @@ int main(int argc, char *argv[]) /* Print output based on format */ switch (format) { + case PLAIN: /* Use same code as CSV */ case CSV: /* Print out column names */ if (!no_column_flag->answer) { diff --git a/vector/v.profile/v.profile.md b/vector/v.profile/v.profile.md index c871cfc34a2..02c82200b79 100644 --- a/vector/v.profile/v.profile.md +++ b/vector/v.profile/v.profile.md @@ -65,19 +65,20 @@ v.profile input=streams@PERMANENT output=/home/user/river_profile.csv \ east_north=600570.27364,4920613.41838,600348.034348,4920840.38617 ``` -### Default Output +### CSV Output ```sh v.profile input=firestations@PERMANENT buffer=100 \ - profile_map=railroads@PERMANENT profile_where="cat=3202" + profile_map=railroads@PERMANENT profile_where="cat=3202" \ + format=csv ``` The output looks as follows: ```text -Number|Distance|cat|ID|LABEL|LOCATION|CITY|MUN_COUNT|PUMPERS|PUMPER_TAN|TANKER|MINI_PUMPE|RESCUE_SER|AERIAL|BRUSH|OTHERS|WATER_RESC|MUNCOID|BLDGCODE|AGENCY|STATIONID|RECNO|CV_SID2|CVLAG -1|2750.44|60|42|"Morrisville #2"|"10632 Chapel Hill Rd"|"Morrisville"|"M"|0|1|0|0|0|1|0|1|0|1|"298"|"FD"|"MF2A"|62|"MF2A"|1.4 -2|5394.27|2|23|"Morrisville #1"|"100 Morrisville-Carpenter Rd"|"Morrisville"|"M"|0|1|0|0|1|0|1|3|0|1|"241"|"FD"|"MF1A"|2|"MF1A"|1.4 +Number,Distance,cat,ID,LABEL,LOCATION,CITY,MUN_COUNT,PUMPERS,PUMPER_TAN,TANKER,MINI_PUMPE,RESCUE_SER,AERIAL,BRUSH,OTHERS,WATER_RESC,MUNCOID,BLDGCODE,AGENCY,STATIONID,RECNO,CV_SID2,CVLAG +1,2750.44,60,42,"Morrisville #2","10632 Chapel Hill Rd","Morrisville","M",0,1,0,0,0,1,0,1,0,1,"298","FD","MF2A",62,"MF2A",1.4 +2,5394.27,2,23,"Morrisville #1","100 Morrisville-Carpenter Rd","Morrisville","M",0,1,0,0,1,0,1,3,0,1,"241","FD","MF1A",2,"MF1A",1.4 ``` **Note:** You can use different delimiters with the `separator=` option From f16a9a49a489b997f7de7a15422748ddfc5af4eb Mon Sep 17 00:00:00 2001 From: SAKET KUMAR MALL <72020337+saket0187@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:59:37 +0530 Subject: [PATCH 9/9] Update vector/v.profile/main.c Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- vector/v.profile/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/v.profile/main.c b/vector/v.profile/main.c index a54b0f3cf03..b8181211052 100644 --- a/vector/v.profile/main.c +++ b/vector/v.profile/main.c @@ -419,8 +419,8 @@ int main(int argc, char *argv[]) /* the field separator */ if (delim_opt->count == 0 && format == CSV) { - delim_opt->answer = "comma"; - } + delim_opt->answer = "comma"; + } fs = G_option_to_separator(delim_opt);