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/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..b8181211052 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 @@ -47,6 +47,8 @@ * 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; @@ -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;Plain text with pipe separator;" + "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,20 @@ 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 || \ @@ -388,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 */ @@ -677,84 +711,191 @@ 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: /* Use same code as CSV */ + 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)); + } } + 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); - } - 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)); + /* 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); + 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 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)); + } } + 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; + + 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); + } else { - for (col = 0; col < ncols; col++) { - fprintf(ascii, "%s", fs); + 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); } } } - db_close_cursor(&cursor); + G_json_array_append_value(root_array, item_value); } - /* Terminate attribute data line and flush data to provided output - * (file/stdout) */ - fprintf(ascii, "\n"); + /* 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); + /* 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; } /* Build topology for vector map and close them */ diff --git a/vector/v.profile/testsuite/test_v_profile.py b/vector/v.profile/testsuite/test_v_profile.py index d7f882492e2..82d70204f86 100644 --- a/vector/v.profile/testsuite/test_v_profile.py +++ b/vector/v.profile/testsuite/test_v_profile.py @@ -12,6 +12,7 @@ Cover more input/output combinations. """ +import json from pathlib import Path from grass.gunittest.case import TestCase @@ -233,6 +234,54 @@ 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""" diff --git a/vector/v.profile/v.profile.md b/vector/v.profile/v.profile.md index 3471fdc9578..02c82200b79 100644 --- a/vector/v.profile/v.profile.md +++ b/vector/v.profile/v.profile.md @@ -2,7 +2,7 @@ *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. +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 @@ -65,6 +65,96 @@ 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 +``` + +**Note:** You can use different delimiters with the `separator=` option +(e.g., `separator=comma`, `separator=tab`, `separator=space`). + +### 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