diff --git a/temporal/t.rast3d.univar/t.rast3d.univar.py b/temporal/t.rast3d.univar/t.rast3d.univar.py index 2cdadd2aaf6..52a235c032b 100755 --- a/temporal/t.rast3d.univar/t.rast3d.univar.py +++ b/temporal/t.rast3d.univar/t.rast3d.univar.py @@ -34,10 +34,10 @@ # %option G_OPT_STR3DS_INPUT # %end -# %option G_OPT_R_INPUT +# %option G_OPT_R3_INPUT # % key: zones # % label: Raster map with zones to compute statistics for -# % description: Raster map with zones to compute statistics for (needs to be CELL) +# % description: 3D raster map with zones # % required: no # %end @@ -49,12 +49,37 @@ # % required: no # %end +# %option +# % key: percentile +# % type: double +# % required: no +# % multiple: yes +# % options: 0-100 +# % description: Percentile to calculate (requires extended statistics flag) +# %end + # %option G_OPT_T_WHERE # % guisection: Selection # %end +# %option +# % key: region_relation +# % description: Process only maps with this spatial relation to the current computational region +# % guisection: Selection +# % options: overlaps,contains,is_contained +# % required: no +# % multiple: no +# %end + # %option G_OPT_F_SEP # % label: Field separator character between the output columns +# % answer: +# % guisection: Formatting +# %end + +# %option G_OPT_F_FORMAT +# % options: plain,json,csv +# % descriptions: plain;Plain text output;json;JSON (JavaScript Object Notation);csv;CSV (Comma Separated Values) # % guisection: Formatting # %end @@ -69,6 +94,10 @@ # % guisection: Formatting # %end +# %rules +# % requires: percentile,-e +# %end + import grass.script as gs ############################################################################ @@ -78,18 +107,48 @@ def main(): # Get the options options, flags = gs.parser() - # lazy imports - import grass.temporal as tgis - # Define variables input = options["input"] zones = options["zones"] output = options["output"] nprocs = gs.resolve_nprocs(options["nprocs"]) where = options["where"] + region_relation = options["region_relation"] extended = flags["e"] no_header = flags["s"] separator = gs.separator(options["separator"]) + output_format = options.get("format", "plain") + + if output_format == "csv": + if not separator: + separator = "," + elif len(separator) > 1: + gs.fatal( + _("A standard CSV separator (delimiter) is only one character long") + ) + + elif output_format == "json": + if no_header: + gs.fatal(_("Column names are always included in JSON output")) + if separator: + gs.fatal(_("Separator option is not allowed with JSON format")) + + elif not separator: + separator = "|" + + percentile = None + if options["percentile"]: + try: + percentile = list(map(float, options["percentile"].split(","))) + except ValueError: + gs.fatal( + _("<{}> is not valid input to the percentile option").format( + options["percentile"] + ) + ) + + # lazy imports + import grass.temporal as tgis # Make sure the temporal database exists tgis.init() @@ -99,18 +158,20 @@ def main(): if output == "-": output = None - # Check if zones map exists and is of type CELL tgis.print_gridded_dataset_univar_statistics( "str3ds", input, output, where, extended, + percentile=percentile, no_header=no_header, fs=separator, rast_region=False, + region_relation=region_relation, zones=zones, nprocs=nprocs, + format=output_format, ) diff --git a/temporal/t.rast3d.univar/testsuite/test_t_rast3d_univar.py b/temporal/t.rast3d.univar/testsuite/test_t_rast3d_univar.py index 4bc6985aa8f..023dbd6726b 100644 --- a/temporal/t.rast3d.univar/testsuite/test_t_rast3d_univar.py +++ b/temporal/t.rast3d.univar/testsuite/test_t_rast3d_univar.py @@ -8,6 +8,7 @@ @author Soeren Gebbert """ +import json from pathlib import Path from grass.gunittest.case import TestCase @@ -254,6 +255,121 @@ def test_with_zones_parallel(self): res_line = res.split("|", 1)[1] self.assertLooksLike(ref_line, res_line) + def test_format_csv(self): + """Test output generation in CSV format""" + t_rast3d_univar = SimpleModule( + "t.rast3d.univar", + input="A", + where="start_time >= '2001-01-01'", + format="csv", + overwrite=True, + verbose=True, + ) + self.assertModule(t_rast3d_univar) + + univar_text = """id,start,end,non_null_cells,null_cells,min,max,range,mean,mean_of_abs,stddev,variance,coeff_var,sum,sum_abs +a_1@testing,2001-01-01 00:00:00,2001-04-01 00:00:00,480000,0,100,100,0,100,100,0,0,0,48000000,48000000 +a_2@testing,2001-04-01 00:00:00,2001-07-01 00:00:00,480000,0,200,200,0,200,200,0,0,0,96000000,96000000 +a_3@testing,2001-07-01 00:00:00,2001-10-01 00:00:00,480000,0,300,300,0,300,300,0,0,0,144000000,144000000 +a_4@testing,2001-10-01 00:00:00,2002-01-01 00:00:00,480000,0,400,400,0,400,400,0,0,0,192000000,192000000 +""" + for ref, res in zip( + univar_text.split("\n"), + t_rast3d_univar.outputs.stdout.split("\n"), + strict=True, + ): + if ref and res: + ref_line = ref.split(",", 1)[1] + res_line = res.split(",", 1)[1] + self.assertLooksLike(ref_line, res_line) + + def test_format_json(self): + """Test output generation in JSON format""" + t_rast3d_univar = SimpleModule( + "t.rast3d.univar", + input="A", + where="start_time >= '2001-04-01 00:00:00'", + format="json", + overwrite=True, + verbose=True, + ) + self.assertModule(t_rast3d_univar) + + output_json = json.loads(t_rast3d_univar.outputs.stdout) + + expected_json = [ + { + "id": "a_2", + "start": "2001-04-01 00:00:00", + "end": "2001-07-01 00:00:00", + "mean": 200, + "min": 200, + "max": 200, + "range": 0, + "mean_of_abs": 200, + "stddev": 0, + "variance": 0, + "coeff_var": 0, + "sum": 96000000, + "null_cells": 0, + "cells": 480000, + "n": 480000, + }, + { + "id": "a_3", + "start": "2001-07-01 00:00:00", + "end": "2001-10-01 00:00:00", + "mean": 300, + "min": 300, + "max": 300, + "range": 0, + "mean_of_abs": 300, + "stddev": 0, + "variance": 0, + "coeff_var": 0, + "sum": 144000000, + "null_cells": 0, + "cells": 480000, + "n": 480000, + }, + { + "id": "a_4", + "start": "2001-10-01 00:00:00", + "end": "2002-01-01 00:00:00", + "mean": 400, + "min": 400, + "max": 400, + "range": 0, + "mean_of_abs": 400, + "stddev": 0, + "variance": 0, + "coeff_var": 0, + "sum": 192000000, + "null_cells": 0, + "cells": 480000, + "n": 480000, + }, + ] + + self.assertEqual(len(output_json), len(expected_json)) + + for res_obj, exp_obj in zip(output_json, expected_json, strict=True): + for key in exp_obj: + if key == "id": + self.assertTrue(res_obj[key].startswith(exp_obj[key])) + else: + self.assertEqual(res_obj[key], exp_obj[key]) + + def test_format_validation(self): + """Test format validation errors (e.g. JSON with separator or no header)""" + self.assertModuleFail("t.rast3d.univar", input="A", format="json", flags="s") + self.assertModuleFail( + "t.rast3d.univar", input="A", format="json", separator="," + ) + self.assertModuleFail( + "t.rast3d.univar", input="A", format="csv", separator="::" + ) + if __name__ == "__main__": from grass.gunittest.main import test