Skip to content

Commit

Permalink
r.object.geometry: add json support (OSGeo#4105)
Browse files Browse the repository at this point in the history
* r.object.geometry: add json support

* debug test failure in ci

* fix tests
  • Loading branch information
kritibirda26 authored Aug 19, 2024
1 parent 2cf98da commit 4d17eb5
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 39 deletions.
2 changes: 1 addition & 1 deletion raster/r.object.geometry/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../..

PGM = r.object.geometry

LIBES = $(RASTERLIB) $(GISLIB)
LIBES = $(RASTERLIB) $(GISLIB) $(PARSONLIB)
DEPENDENCIES = $(RASTERDEP) $(GISDEP)

include $(MODULE_TOPDIR)/include/Make/Module.make
Expand Down
111 changes: 86 additions & 25 deletions raster/r.object.geometry/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <grass/gis.h>
#include <grass/raster.h>
#include <grass/glocale.h>
#include <grass/parson.h>

enum OutputFormat { PLAIN, JSON };

/* compare two cell values
* return 0 if equal, 1 if different */
Expand All @@ -42,6 +45,7 @@ int main(int argc, char *argv[])
struct Option *opt_in;
struct Option *opt_out;
struct Option *opt_sep;
struct Option *fmt_opt;
struct Flag *flag_m;
char *sep;
FILE *out_fp;
Expand All @@ -59,6 +63,11 @@ int main(int argc, char *argv[])
int planimetric = 0, compute_areas = 0;
struct Cell_head cellhd;

enum OutputFormat format;
JSON_Array *root_array;
JSON_Object *object;
JSON_Value *root_value, *object_value;

G_gisinit(argv[0]);

/* Define the different options */
Expand All @@ -82,10 +91,22 @@ int main(int argc, char *argv[])
flag_m->key = 'm';
flag_m->label = _("Use meters as units instead of cells");

fmt_opt = G_define_standard_option(G_OPT_F_FORMAT);
fmt_opt->guisection = _("Print");

/* parse options */
if (G_parser(argc, argv))
exit(EXIT_FAILURE);

if (strcmp(fmt_opt->answer, "json") == 0) {
format = JSON;
root_value = json_value_init_array();
root_array = json_array(root_value);
}
else {
format = PLAIN;
}

sep = G_option_to_separator(opt_sep);
in_fd = Rast_open_old(opt_in->answer, "");

Expand Down Expand Up @@ -294,16 +315,18 @@ int main(int argc, char *argv[])
G_free(prev_in);

G_message(_("Writing output"));
/* print table */
fprintf(out_fp, "cat%s", sep);
fprintf(out_fp, "area%s", sep);
fprintf(out_fp, "perimeter%s", sep);
fprintf(out_fp, "compact_square%s", sep);
fprintf(out_fp, "compact_circle%s", sep);
fprintf(out_fp, "fd%s", sep);
fprintf(out_fp, "mean_x%s", sep);
fprintf(out_fp, "mean_y");
fprintf(out_fp, "\n");
if (format == PLAIN) {
/* print table */
fprintf(out_fp, "cat%s", sep);
fprintf(out_fp, "area%s", sep);
fprintf(out_fp, "perimeter%s", sep);
fprintf(out_fp, "compact_square%s", sep);
fprintf(out_fp, "compact_circle%s", sep);
fprintf(out_fp, "fd%s", sep);
fprintf(out_fp, "mean_x%s", sep);
fprintf(out_fp, "mean_y");
fprintf(out_fp, "\n");
}

/* print table body */
for (i = 0; i < n_objects; i++) {
Expand All @@ -312,22 +335,42 @@ int main(int argc, char *argv[])
if (obj_geos[i].area == 0)
continue;

fprintf(out_fp, "%d%s", min + i, sep);
fprintf(out_fp, "%f%s", obj_geos[i].area, sep);
fprintf(out_fp, "%f%s", obj_geos[i].perimeter, sep);
fprintf(out_fp, "%f%s",
4 * sqrt(obj_geos[i].area) / obj_geos[i].perimeter, sep);
fprintf(out_fp, "%f%s",
obj_geos[i].perimeter / (2 * sqrt(M_PI * obj_geos[i].area)),
sep);
double compact_square =
4 * sqrt(obj_geos[i].area) / obj_geos[i].perimeter;
double compact_circle =
obj_geos[i].perimeter / (2 * sqrt(M_PI * obj_geos[i].area));
/* log 1 = 0, so avoid that by always adding 0.001 to the area: */
fprintf(out_fp, "%f%s",
2 * log(obj_geos[i].perimeter) / log(obj_geos[i].area + 0.001),
sep);
if (!flag_m->answer)
double fd =
2 * log(obj_geos[i].perimeter) / log(obj_geos[i].area + 0.001);
if (!flag_m->answer) {
obj_geos[i].num = obj_geos[i].area;
fprintf(out_fp, "%f%s", obj_geos[i].x / obj_geos[i].num, sep);
fprintf(out_fp, "%f", obj_geos[i].y / obj_geos[i].num);
}
double mean_x = obj_geos[i].x / obj_geos[i].num;
double mean_y = obj_geos[i].y / obj_geos[i].num;
switch (format) {
case PLAIN:
fprintf(out_fp, "%d%s", min + i, sep);
fprintf(out_fp, "%f%s", obj_geos[i].area, sep);
fprintf(out_fp, "%f%s", obj_geos[i].perimeter, sep);
fprintf(out_fp, "%f%s", compact_square, sep);
fprintf(out_fp, "%f%s", compact_circle, sep);
fprintf(out_fp, "%f%s", fd, sep);
fprintf(out_fp, "%f%s", mean_x, sep);
fprintf(out_fp, "%f", mean_y);
break;
case JSON:
object_value = json_value_init_object();
object = json_object(object_value);
json_object_set_number(object, "category", min + i);
json_object_set_number(object, "area", obj_geos[i].area);
json_object_set_number(object, "perimeter", obj_geos[i].perimeter);
json_object_set_number(object, "compact_square", compact_square);
json_object_set_number(object, "compact_circle", compact_circle);
json_object_set_number(object, "fd", fd);
json_object_set_number(object, "mean_x", mean_x);
json_object_set_number(object, "mean_y", mean_y);
break;
}
/* object id: i + min */

/* TODO */
Expand All @@ -342,8 +385,26 @@ int main(int argc, char *argv[])

/* variance of X and Y to approximate bounding ellipsoid */

fprintf(out_fp, "\n");
switch (format) {
case PLAIN:
fprintf(out_fp, "\n");
break;
case JSON:
json_array_append_value(root_array, object_value);
break;
}
}

if (format == JSON) {
char *serialized_string = json_serialize_to_string_pretty(root_value);
if (serialized_string == NULL) {
G_fatal_error(_("Failed to initialize pretty JSON string."));
}
puts(serialized_string);
json_free_serialized_string(serialized_string);
json_value_free(root_value);
}

if (out_fp != stdout)
fclose(out_fp);

Expand Down
71 changes: 71 additions & 0 deletions raster/r.object.geometry/r.object.geometry.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,77 @@ <h2>EXAMPLE</h2>
r.object.geometry input=soilsID output=soils_geom.txt
</pre></div>

The <b>format=json</b> option can be used to change the output format to JSON:

<div class="code"><pre>
r.object.geometry input=zipcodes format=json
</pre></div>

<div class="code"><pre>
[
{
"category": 1,
"area": 106,
"perimeter": 62,
"compact_circle": 1.6987670351864215,
"compact_square": 0.66423420264432265,
"fd": 1.7699924681225903,
"mean_x": 631382.07547169807,
"mean_y": 222764.15094339623
},
{
"category": 2,
"area": 57,
"perimeter": 36,
"compact_circle": 1.3451172460704992,
"compact_square": 0.83887049280786108,
"fd": 1.772672742164326,
"mean_x": 643460.52631578944,
"mean_y": 217232.45614035087
},
{
"category": 3,
"area": 10,
"perimeter": 16,
"compact_circle": 1.4272992929222168,
"compact_square": 0.79056941504209488,
"fd": 2.4081353865496951,
"mean_x": 631300,
"mean_y": 215450
},
{
"category": 4,
"area": 63,
"perimeter": 60,
"compact_circle": 2.1324361862292305,
"compact_square": 0.52915026221291817,
"fd": 1.9764401337147652,
"mean_x": 642345.23809523811,
"mean_y": 226599.20634920636
},
{
"category": 5,
"area": 491,
"perimeter": 156,
"compact_circle": 1.9859985189304281,
"compact_square": 0.56816717451693177,
"fd": 1.6299200778082998,
"mean_x": 637912.93279022397,
"mean_y": 220636.96537678209
},
{
"category": 6,
"area": 83,
"perimeter": 60,
"compact_circle": 1.8578355639603314,
"compact_square": 0.60736223860961991,
"fd": 1.8531256328449071,
"mean_x": 635846.38554216863,
"mean_y": 227219.8795180723
}
]
</pre></div>

<h2>SEE ALSO</h2>

<em>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
module.
"""

import json
import os
from sys import stderr

from grass.gunittest.case import TestCase
from grass.gunittest.main import test
from grass.gunittest.gmodules import call_module

from grass.gunittest.gmodules import SimpleModule

testraster1 = """\
north: 250000
Expand All @@ -30,31 +36,25 @@ class TestObjectGeometryPixel(TestCase):
test_objects1 = "test_objects1"
output_file_pixel = "output_file_pixel.csv"

@classmethod
def setUpClass(cls):
def setUp(self):
"""Imports test raster(s), ensures expected computational region and setup"""
cls.runModule(
self.runModule(
"r.in.ascii",
input="-",
type="CELL",
stdin_=testraster1,
output=cls.test_objects1,
output=self.test_objects1,
)
cls.use_temp_region()
cls.runModule("g.region", raster=cls.test_objects1)

@classmethod
def tearDownClass(cls):
"""Remove the temporary region"""
cls.del_temp_region()
self.use_temp_region()
self.runModule("g.region", raster=self.test_objects1)

def tearDown(self):
"""Remove the outputs created from the object geometry module
"""Remove the outputs created from the object geometry module and the temporary region
This is executed after each test run.
"""
if os.path.isfile(self.output_file_pixel):
os.remove(self.output_file_pixel)
self.del_temp_region()
self.runModule("g.remove", flags="f", type="raster", name=self.test_objects1)

def test_object_geometry_pixel(self):
Expand All @@ -72,6 +72,47 @@ def test_object_geometry_pixel(self):
msg="Output file is not equal to reference file",
)

def test_object_geometry_json(self):
"""Test json format output"""
reference = [
{
"category": 1,
"area": 4,
"perimeter": 8,
"compact_circle": 1.1283791670955126,
"compact_square": 1,
"fd": 2.999459154496928,
"mean_x": 625000,
"mean_y": 237500,
},
{
"category": 2,
"area": 8,
"perimeter": 12,
"compact_circle": 1.1968268412042982,
"compact_square": 0.94280904158206347,
"fd": 2.3898313512153728,
"mean_x": 655000,
"mean_y": 225000,
},
{
"category": 3,
"area": 4,
"perimeter": 8,
"compact_circle": 1.1283791670955126,
"compact_square": 1,
"fd": 2.999459154496928,
"mean_x": 625000,
"mean_y": 212500,
},
]
module = SimpleModule(
"r.object.geometry", input=self.test_objects1, format="json"
)
self.runModule(module)
data = json.loads(module.outputs.stdout)
self.assertCountEqual(reference, data)


class TestObjectGeometryMeter(TestCase):
"""Test case for object geometry module"""
Expand Down

0 comments on commit 4d17eb5

Please sign in to comment.