Skip to content
Open
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
3 changes: 2 additions & 1 deletion general/g.parser/standard_option.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ static char *STD_OPT_STRINGS[] = {"G_OPT_UNDEFINED",
"G_OPT_T_SUFFIX",
"G_OPT_T_TYPE",
"G_OPT_T_WHERE",
"G_OPT_T_SAMPLE"};
"G_OPT_T_SAMPLE",
"G_OPT_F_FORMAT"};

struct Option *define_standard_option(const char *name)
{
Expand Down
31 changes: 31 additions & 0 deletions python/grass/script/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

from __future__ import annotations

import datetime
import json
import os
import shutil
import locale
Expand Down Expand Up @@ -693,3 +695,32 @@ def append_random(name, suffix_length=None, total_length=None):
# The following can be shorter with random.choices from Python 3.6.
suffix = "".join(random.choice(allowed_chars) for _ in range(suffix_length))
return "{name}_{suffix}".format(**locals())


class TemporalJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder with datetime support for GRASS GIS.

Handles serialization of datetime objects to ISO 8601 format strings.
Can be used across GRASS modules that need JSON output with temporal data.

Example:

.. code-block:: pycon

>>> import json
>>> from datetime import datetime
>>> data = {"timestamp": datetime(2024, 1, 15, 10, 30)}
>>> json.dumps(data, cls=TemporalJSONEncoder)
'{"timestamp": "2024-01-15T10:30:00"}'
"""

def default(self, obj):
"""Override default JSON encoding for datetime objects.

:param obj: Object to encode
:return: ISO 8601 formatted string for datetime objects,
or calls parent encoder for other types
"""
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return super().default(obj)
2 changes: 1 addition & 1 deletion python/grass/temporal/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GDIR = $(PYDIR)/grass
DSTDIR = $(GDIR)/temporal
DSTDIRPLY = $(DSTDIR)/ply

MODULES = ply/__init__ ply/lex ply/yacc base core abstract_dataset abstract_map_dataset abstract_space_time_dataset space_time_datasets open_stds factory gui_support list_stds register sampling metadata spatial_extent temporal_extent datetime_math temporal_granularity spatio_temporal_relationships unit_tests aggregation stds_export stds_import extract mapcalc univar_statistics temporal_topology_dataset_connector spatial_topology_dataset_connector c_libraries_interface temporal_algebra temporal_vector_algebra temporal_raster_base_algebra temporal_raster_algebra temporal_raster3d_algebra temporal_operator
MODULES = ply/__init__ ply/lex ply/yacc base core abstract_dataset abstract_map_dataset abstract_space_time_dataset space_time_datasets open_stds factory gui_support list_stds register sampling metadata spatial_extent temporal_extent datetime_math temporal_granularity spatio_temporal_relationships unit_tests aggregation stds_export stds_import extract mapcalc univar_statistics temporal_topology_dataset_connector spatial_topology_dataset_connector c_libraries_interface temporal_algebra temporal_vector_algebra temporal_raster_base_algebra temporal_raster_algebra temporal_raster3d_algebra temporal_operator utils

CLEAN_SUBDIRS = ply

Expand Down
2 changes: 2 additions & 0 deletions python/grass/temporal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
print_gridded_dataset_univar_statistics,
print_vector_dataset_univar_statistics,
)
from grass.script.utils import TemporalJSONEncoder

__all__ = [
"AbsoluteTemporalExtent",
Expand Down Expand Up @@ -253,6 +254,7 @@
"TemporalAlgebraLexer",
"TemporalAlgebraParser",
"TemporalExtent",
"TemporalJSONEncoder",
"TemporalOperatorLexer",
"TemporalOperatorParser",
"TemporalRaster3DAlgebraParser",
Expand Down
51 changes: 51 additions & 0 deletions python/grass/temporal/abstract_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@

from __future__ import annotations

import json
from abc import ABCMeta, abstractmethod

from .core import get_current_mapset, get_tgis_message_interface, init_dbif
from .spatial_topology_dataset_connector import SpatialTopologyDatasetConnector
from .temporal_topology_dataset_connector import TemporalTopologyDatasetConnector
from grass.script.utils import TemporalJSONEncoder


###############################################################################

Expand Down Expand Up @@ -218,6 +221,54 @@ def print_info(self):
def print_shell_info(self):
"""Print information about this class in shell style"""

def print_json(self) -> None:
"""Print dataset metadata as JSON to stdout.

Outputs complete metadata in JSON format by merging all internal
data dictionaries. This automatically includes all metadata fields
without manual enumeration, ensuring parity with shell output.

The output can be parsed by any JSON-compatible tool or library.
"""
data = self._to_json_dict()
print(json.dumps(data, cls=TemporalJSONEncoder, indent=4))

def _to_json_dict(self):
"""Build a dictionary from internal metadata storage for JSON output.

Uses dict merging to automatically capture all metadata from the
internal D dictionaries. This approach ensures:

- Complete metadata coverage (no missing fields)
- No manual key enumeration required
- Automatic updates when new metadata fields are added
- Parity with print_shell_info() output

:return: Complete metadata dictionary ready for JSON serialization
"""
# Merge all internal data dictionaries.
# Each component (base, temporal_extent, spatial_extent, metadata)
# stores its data in a D dict that gets merged here.
data = {}

# Add base metadata (id, name, mapset, creator, etc.)
if hasattr(self, "base") and hasattr(self.base, "D"):
data.update(self.base.D)

# Add temporal extent metadata (start_time, end_time, etc.)
if hasattr(self, "temporal_extent") and hasattr(self.temporal_extent, "D"):
data.update(self.temporal_extent.D)

# Add spatial extent metadata (north, south, east, west, etc.)
if hasattr(self, "spatial_extent") and hasattr(self.spatial_extent, "D"):
data.update(self.spatial_extent.D)

# Add general metadata (title, description, etc.)
if hasattr(self, "metadata") and hasattr(self.metadata, "D"):
data.update(self.metadata.D)

return data

@abstractmethod
def print_self(self):
"""Print the content of the internal structure to stdout"""
Expand Down
18 changes: 18 additions & 0 deletions python/grass/temporal/abstract_map_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,24 @@ def print_shell_info(self) -> None:
if self.is_topology_build():
self.print_topology_shell_info()

def _to_json_dict(self):
"""Build a dictionary from internal metadata storage for JSON output.

Extends the base implementation by adding information about
space-time datasets that this map is registered in.

:return: Complete metadata dictionary including STDS registration info
"""
# Get base metadata from parent class (uses dict-merge approach)
data = super()._to_json_dict()

# Add map-specific information: which STDSs is this map registered in?
datasets = self.get_registered_stds()
if datasets:
data["registered_datasets"] = sorted(list(datasets))

return data

def insert(self, dbif=None, execute: bool = True):
"""Insert the map content into the database from the internal
structure
Expand Down
23 changes: 22 additions & 1 deletion python/grass/temporal/abstract_space_time_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class that is the base class for all space time datasets.
from pathlib import Path
from grass.exceptions import FatalError

from .abstract_dataset import AbstractDataset, AbstractDatasetComparisonKeyStartTime
from .abstract_dataset import (
AbstractDataset,
AbstractDatasetComparisonKeyStartTime,
)
from .core import (
get_current_mapset,
get_sql_template_path,
Expand Down Expand Up @@ -178,6 +181,24 @@ def print_shell_info(self) -> None:
self.spatial_extent.print_shell_info()
self.metadata.print_shell_info()

def _to_json_dict(self):
"""Build a dictionary from internal metadata storage for JSON output.

Extends the base implementation by adding computed fields that are
present in shell output but not stored in the D dictionaries.
For example, semantic_labels is fetched on-the-fly via SQL.

:return: Complete metadata dictionary with computed fields included
"""
data = super()._to_json_dict()

# Add computed fields from metadata that are in shell output
# but not in metadata.D (fetched on-the-fly via SQL)
if hasattr(self.metadata, "get_semantic_labels"):
data["semantic_labels"] = self.metadata.get_semantic_labels()

return data

def print_history(self) -> None:
"""Print history information about this class in human readable
shell style
Expand Down
14 changes: 14 additions & 0 deletions python/grass/temporal/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Utility functions and classes for the temporal framework.

(C) 2025 by the GRASS Development Team
This program is free software under the GNU General Public
License (>=v2). Read the file COPYING that comes with GRASS
for details.

:authors: GRASS Development Team
"""

# Re-export from canonical location for backward compatibility
from grass.script.utils import TemporalJSONEncoder

__all__ = ["TemporalJSONEncoder"]
11 changes: 10 additions & 1 deletion temporal/t.info/t.info.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
# % options: strds, str3ds, stvds, raster, raster_3d, vector
# %end

# %option G_OPT_F_FORMAT
# % options: plain,json,shell
# % descriptions: plain;Human readable text output;shell;Shell script style text output;json;JSON (JavaScript Object Notation)
# % guisection: Print
# %end

# %flag
# % key: g
# % description: Print in shell script style
Expand Down Expand Up @@ -65,6 +71,7 @@ def main():

name = options["input"]
type_ = options["type"]
format_ = options["format"]
shellstyle = flags["g"]
system = flags["d"]
history = flags["h"]
Expand Down Expand Up @@ -119,7 +126,9 @@ def main():
dataset.print_history()
return

if shellstyle:
if format_ == "json":
dataset.print_json()
elif format_ == "shell" or shellstyle:
dataset.print_shell_info()
else:
dataset.print_info()
Expand Down
Loading
Loading