Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion raster/r.geomorphon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../..
PGM = r.geomorphon

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

include $(MODULE_TOPDIR)/include/Make/Module.make

Expand Down
148 changes: 93 additions & 55 deletions raster/r.geomorphon/profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "local_proto.h"
#include <grass/gjson.h>

#define JSON_MIN_INDENT 1
#define YAML_MIN_INDENT 0
#define XML_MIN_INDENT 1
#define MAX_STR_LEN GNAME_MAX
Expand Down Expand Up @@ -252,90 +253,127 @@ static const char *format_token_common(const struct token *t)
static unsigned write_json(FILE *f)
{
unsigned i;
G_JSON_Value *root_val;
G_JSON_Object *obj_stack[MAX_STACK_ELEMS];
int depth = 0;

root_val = G_json_value_init_object();
if (root_val == NULL) {
G_fatal_error(_("Failed to initialize JSON object. Out of memory?"));
}

obj_stack[0] = G_json_value_get_object(root_val);
G_JSON_Value *stack_vals[MAX_STACK_ELEMS];
unsigned stack_top = 0;
G_JSON_Value *root = G_json_value_init_object();
if (!root)
return 0;
stack_vals[stack_top++] = root;

for (i = 0; i < size; i++) {
G_JSON_Object *cur = obj_stack[depth];
G_JSON_Object *parent = G_json_object(stack_vals[stack_top - 1]);
if (!parent) {
G_json_value_free(root);
return 0;
}

switch (token[i].type) {
case T_SSO: {
G_JSON_Value *sub_val = G_json_value_init_object();
if (sub_val == NULL) {
G_fatal_error(
_("Failed to initialize JSON object. Out of memory?"));
if (stack_top >= MAX_STACK_ELEMS) {
G_json_value_free(root);
return 0;
}
G_JSON_Object *sub_obj = G_json_value_get_object(sub_val);
G_json_object_set_value(cur, token[i].key, sub_val);

if (depth < MAX_STACK_ELEMS - 1) {
obj_stack[++depth] = sub_obj;
G_JSON_Value *obj = G_json_value_init_object();
if (!obj) {
G_json_value_free(root);
return 0;
}
else {
G_warning(_("JSON nesting exceeds maximum depth"));
G_json_value_free(root_val);
if (G_json_object_set_value(parent, token[i].key, obj) !=
G_JSONSuccess) {
G_json_value_free(obj);
G_json_value_free(root);
return 0;
}
break;
}
stack_vals[stack_top++] = obj;
} break;
case T_ESO:
if (depth > 0) {
depth--;
if (stack_top <= 1) {
/* can't pop root */
G_json_value_free(root);
return 0;
}
else if (depth == 0) {
G_json_value_free(root_val);
stack_top--;
break;
case T_BLN:
if (G_json_object_set_boolean(parent, token[i].key,
token[i].int_val ? 1 : 0) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
break;

case T_INT:
G_json_object_set_number(cur, token[i].key,
(double)token[i].int_val);
break;

case T_BLN:
G_json_object_set_boolean(cur, token[i].key, token[i].int_val);
if (G_json_object_set_number(parent, token[i].key,
(double)token[i].int_val) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
break;

case T_DBL:
if (isnan(token[i].dbl_val)) {
if (G_json_object_set_null(parent, token[i].key) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
}
else {
if (G_json_object_set_number(parent, token[i].key,
token[i].dbl_val) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
}
break;
case T_MTR:
if (isnan(token[i].dbl_val))
G_json_object_set_null(cur, token[i].key);
else
G_json_object_set_number(cur, token[i].key, token[i].dbl_val);
if (isnan(token[i].dbl_val)) {
if (G_json_object_set_null(parent, token[i].key) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
}
else {
if (G_json_object_set_number(parent, token[i].key,
token[i].dbl_val) !=
G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
}
break;

case T_STR:
G_json_object_set_string(cur, token[i].key, token[i].str_val);
if (G_json_object_set_string(parent, token[i].key,
token[i].str_val) != G_JSONSuccess) {
G_json_value_free(root);
return 0;
}
break;
default:
G_json_value_free(root_val);
G_json_value_free(root);
return 0;
}
}
if (depth != 0 || overflow) {
G_json_value_free(root_val);

if (stack_top != 1 || overflow) {
G_json_value_free(root);
return 0;
}
char *json_str = G_json_serialize_to_string_pretty(root_val);
if (!json_str) {
G_json_value_free(root_val);

char *s = G_json_serialize_to_string_pretty(root);
if (!s) {
G_json_value_free(root);
return 0;
}

fputs(json_str, f);
fputc('\n', f);

G_json_free_serialized_string(json_str);
G_json_value_free(root_val);
if (fprintf(f, "%s\n", s) < 0) {
G_json_free_serialized_string(s);
G_json_value_free(root);
return 0;
}
G_json_free_serialized_string(s);
G_json_value_free(root);
return 1;
}

Expand Down
77 changes: 37 additions & 40 deletions raster/r.geomorphon/testsuite/test_r_geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
from grass.gunittest.case import TestCase
from grass.gunittest.main import test
from grass.script.core import read_command
import json
import os
from grass.script import core as gcore
import tempfile
import pathlib
from grass.tools import Tools

synth_out = """1 flat
Expand Down Expand Up @@ -299,47 +304,39 @@ def test_sint(self):
self.assertEqual(first=synth_out, second=category)

def test_json_profile(self):
"""Test JSON profile output structure and key values"""
result = self.tools.r_geomorphon(
elevation=self.inele,
coordinates="636400,221100",
profiledata="-",
profileformat="json",
search=10,
)

out = result.stdout

# ensure region is set to the test elevation
self.runModule("g.region", raster=self.inele)
region = gcore.region()
east = float(region.get("east", region["e"]))
west = float(region.get("west", region["w"]))
north = float(region.get("north", region["n"]))
south = float(region.get("south", region["s"]))
e = (east + west) / 2.0
n = (north + south) / 2.0
fd, tmp = tempfile.mkstemp(prefix="rgeom_profile_", suffix=".json")
os.close(fd)
Comment on lines +316 to +317

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are you creating it here?

try:
json_out = json.loads(out)
except ValueError:
self.fail(msg="r.geomorphon produced invalid JSON:\n" + out)

expected = json.loads(expected_json)
for block in expected:
if isinstance(expected[block], dict):
for key in expected[block]:
if isinstance(expected[block][key], dict):
for sub_key in expected[block][key]:
if isinstance(expected[block][key][sub_key], float):
self.assertAlmostEqual(
expected[block][key][sub_key],
json_out[block][key][sub_key],
places=4,
)
else:
self.assertEqual(
expected[block][key][sub_key],
json_out[block][key][sub_key],
)
elif isinstance(expected[block][key], float):
self.assertAlmostEqual(
expected[block][key], json_out[block][key], places=4
)
else:
self.assertEqual(expected[block][key], json_out[block][key])
else:
self.assertEqual(expected[block], json_out[block])
self.runModule(
"r.geomorphon",
elevation=self.inele,
search=3,
profiledata=tmp,
profileformat="json",
coordinates=(e, n),
overwrite=True,
)
self.assertTrue(pathlib.Path(tmp).exists())
self.assertGreater(pathlib.Path(tmp).stat().st_size, 0)
with open(tmp, encoding="utf-8") as fh:
data = json.load(fh)
# basic sanity checks on JSON structure
self.assertIn("final_results", data)
self.assertIn("computation_parameters", data)
self.assertIsInstance(data["final_results"], dict)
self.assertIsInstance(data["computation_parameters"], dict)
Comment on lines +332 to +336

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actual (current) values will need to be checked.

finally:
if pathlib.Path(tmp).exists():
os.remove(tmp)


if __name__ == "__main__":
Expand Down
Loading