The problem

GRASS stores two kinds of thing in two different places. Ordinary maps (rasters, vectors, 3D rasters) live in the spatial database. Space-time datasets (STRDS for raster series, STVDS for vector series, STR3DS for 3D raster series) live in a separate temporal database, and are managed by the t.* family of modules.

The Data Catalog, the tree view inside the GRASS GUI that people use to browse projects, mapsets and maps, only ever queried the spatial database. It bypassed the temporal database entirely.

The practical consequence: if you worked with time series, the GUI showed you every individual map that made up your dataset, but never the dataset itself. Everything about managing them, creating, registering maps, renaming, deleting, inspecting metadata, had to happen on the command line, in a program whose main selling point to many of its users is that it has a GUI.

Why it mattered

Temporal analysis is one of GRASS’s larger capabilities, and the tooling around it is mature. The gap was purely one of surface: the data existed, the modules existed, but nothing connected them to the interface.

That produced two costs. Discoverability: a GUI user had no way to find out that space-time datasets were a thing GRASS could do. And workflow: anyone who did know had to keep a terminal open alongside the GUI and mentally reconcile two different views of the same project.

Approach

I did not start on the GUI. Three things had to be true before the integration was worth building, and the first several weeks went into them.

The first was understanding the temporal framework properly. Community bonding and the opening weeks of coding went on the framework and its documentation, picking up small issues along the way. The first pull request I opened on the project was a documentation fix I found while reading.

The second was getting structured output out of the underlying tools. A GUI has to parse what the temporal modules produce, and scraping formatted text is a losing game. I added JSON and format options across several of them, t.list and t.rast.univar among them, which took five or six pull requests over a week and turned up a run of pre-existing bugs in code paths nobody had exercised much.

The third was knowing what those modules cost, since the catalog would call them constantly. That investigation is under Challenges , and it did not go the way any of us expected.

Only then did I open the main pull request. From that point most of the project happened inside it, with the description rewritten as the work moved.

What I built

Datasets now appear in their mapset trees alongside spatial maps. The first working version had to fight the tooling to get there: t.list had no mapset="*" option at the time, so listing datasets across every mapset meant temporarily rewriting the user’s search path with g.mapset, running t.list, and putting the search path back. I shipped that, and one of my mentors then added mapset="*" upstream, which collapsed the whole workaround into a single call per dataset type.

Context menus came next, on dataset nodes and on the maps registered inside them, covering creation, renaming, deletion and the management operations the community picked out as most useful. Which of those belonged in the first version was settled by discussion with mentors and other GRASS contributors, not by my guessing. Search and filtering were extended at the same time, so the catalog’s existing find-a-map behaviour reaches datasets too.

The loading strategy was the largest single change. My original design listed only the datasets and loaded their maps lazily when a user expanded one. It worked, but it made reloading a mapset and keeping the tree in sync awkward. In week eight my mentor introduced a gui_support module that queries the temporal database with direct SQL, replacing one t.rast.list invocation per dataset. I rebuilt the loading path on top of it. Load times dropped sharply, the sync problems went with them, and it finally became possible to stop maps that were already registered in a dataset from appearing a second time in the standalone list, a duplication visible since the earliest versions.

Two smaller things: module dialogs opened from a context menu now render over the existing GUI, closer to what someone expects when they pick a menu item, where before they spawned a separate window through --ui. And separately from the GUI, t.list gained support for querying several dataset types at once (type=strds,stvds), bringing it in line with g.list.

Challenges

A three-second import that wasn’t

A temporal import used by nearly every t.* module took around three seconds on a mentor’s machine. With the GUI about to call these modules repeatedly, that was a blocker.

My first instinct was lazy imports via __getattr__ in __init__.py. I profiled first instead, and measured 0.17 seconds on the same command, on my machine. I assumed I had measured it wrong, because a 17× difference is not a plausible result. I sent the numbers to my mentors, who were equally surprised and asked me to run their exact command. Same result.

It turned out not to be the import at all. The slowness was a Linux-specific bug; on macOS, where I work, the path was already fast. My mentors fixed the underlying issue. The optimisation I had planned to spend a week on was unnecessary, and would have obscured the real bug had I shipped it.

Animation freezing on large series

While integrating g.gui.animation, the GUI locked up and the machine heated up on animations with many maps, while behaving perfectly on small ones. Before assuming my changes were at fault, I ran the unmodified upstream version and reproduced the same behaviour, which established it as a pre-existing performance problem and not a regression of mine. That became its own pull request.

Knowing when to stop

I started adding support for passing an env parameter through the temporal framework, tgis.init(env=env), expecting a contained change. It reached into several parts of the framework I had not worked in. Pushing a large, half-understood change into a codebase during review season was not worth it, so I paused it and told my mentor why. It is unfinished, deliberately.

Structure decisions that stayed unmade

We considered reworking the Data Catalog’s hierarchy entirely to accommodate datasets more naturally. After weighing it, we decided against it and built on the existing structure. Not every discussion needs to end in a change.

Results

Space-time datasets are visible and manageable inside the GRASS Data Catalog: listed in their mapset trees, searchable, and operable through context menus, without leaving the GUI.

The loading path runs on direct SQL queries now, in place of repeated module invocations, which cut catalog load time and cleared the duplicate-map and tree-sync problems the earlier design carried.

Several improvements landed in the temporal tools themselves: structured output options, multi-type queries, mapset="*" support and a set of bug fixes, all of which are useful to command-line users regardless of the GUI work.

The main pull request is merged.

Working in the open

The project ran on weekly meetings with my mentors, with progress and blockers raised over GitHub and email in between. Design questions went wider than the two of us: which context menu entries to include, how datasets should be represented visually, and whether to restructure the catalog were all discussed with the broader GRASS community, and the answers changed what I built.

Two things from that are worth noting. The performance rewrite in week eight came from a mentor’s module, not from me. My contribution was recognising it solved problems I had been working around and rebuilding on top of it. And when I fell behind in week one due to illness, saying so immediately was more useful than trying to catch up quietly.

I also wrote the project’s final work product documentation on the GRASS Wiki (opens in a new tab) , pitched at someone who has never touched a space-time dataset.