How the HKUDS/CLI-Anything Layout Engine Computes Positions for Nested Group Layers
The CLI-Anything layout engine computes positions for nested group layers by leveraging QGIS's built-in mapLayers() API, which automatically flattens all layers—including deeply nested group children—into a single enumerable collection, then merges their spatial extents into a unified bounding rectangle.
The HKUDS/CLI-Anything repository provides a powerful command-line interface for automating QGIS workflows. A critical component of this system is the layout engine in cli_anything/qgis/core/layouts.py, which handles the spatial positioning of map items when layers are organized into complex group hierarchies. Understanding how this engine computes positions for nested group layers is essential for building reliable, automated cartographic pipelines.
How Nested Group Layers Are Enumerated
The layout engine does not implement custom recursion to traverse group hierarchies. Instead, it relies on QGIS's native project API to handle nested structures.
In _combined_project_extent(), the engine calls project_mod.current_project().mapLayers().values() to retrieve all layers:
# From cli_anything/qgis/core/layouts.py
# Lines 62-75: Layer collection and extent merging
def _combined_project_extent():
project = project_mod.current_project()
extent = None
for layer in project.mapLayers().values():
if layer.type() not in (QgsMapLayer.VectorLayer, QgsMapLayer.RasterLayer):
continue
layer_extent = layer.extent()
if extent is None:
extent = QgsRectangle(layer_extent)
else:
extent.combineExtentWith(layer_extent)
return extent
The key insight: project.mapLayers() returns a flat dictionary containing every layer in the project, regardless of whether it sits at the root level, inside a group, or nested multiple levels deep within subgroups. This eliminates the need for manual tree traversal.
Computing the Combined Extent for Layout Positioning
Once layers are collected, the engine derives a spatial bounding box that encompasses all geographic data. This computed extent directly determines how map items are positioned and scaled within the layout.
The Extent Merging Algorithm
For each eligible layer (vector or raster), the engine:
- Retrieves the layer's
extent()— aQgsRectangledefining its spatial bounds - Merges it into a running aggregate rectangle via
combineExtentWith()
The result is a single QgsRectangle representing the union of all layer extents. This bounding box becomes the default spatial view when no explicit extent is provided to add_map_item().
User-Provided Extents vs. Automatic Computation
The layout engine supports two modes for determining map item boundaries:
| Mode | Method | Use Case |
|---|---|---|
| Automatic | _combined_project_extent() |
Show all project layers, including nested groups |
| Manual | _parse_extent() |
Precise control over the visible map region |
When a user supplies an extent string, _parse_extent() handles the conversion:
# Lines 88-97: Parsing extent strings
def _parse_extent(extent_str):
"""Parse 'xmin,ymin,xmax,ymax' into QgsRectangle."""
parts = extent_str.split(',')
if len(parts) != 4:
raise ValueError(f"Invalid extent format: {extent_str}")
xmin, ymin, xmax, ymax = map(float, parts)
return QgsRectangle(xmin, ymin, xmax, ymax)
Positioning Map Items in the Layout
After determining the spatial extent, add_map_item() positions the map item using layout coordinates:
# Lines 64-68: Map item placement
map_item = QgsLayoutItemMap(layout)
map_item.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
map_item.resizeToContents(QgsLayoutSize(width, height, QgsUnitTypes.LayoutMillimeters))
map_item.setExtent(extent) # From _combined_project_extent() or _parse_extent()
The positioning workflow:
- Spatial coordinates (
x,y,width,height) are specified in millimeters usingQgsLayoutPointandQgsLayoutSize - The extent (computed or parsed) defines what geographic area the map displays
layout_summary(layout)returns a stable representation of the updated layout state (lines 69-70)
Why Nested Groups Require No Special Handling
The CLI-Anything layout engine's elegance lies in its delegation to QGIS's core APIs. Because mapLayers() abstracts away the project hierarchy, the engine treats nested group layers identically to root-level layers:
- No recursive group traversal logic
- No special cases for subgroup depth
- Consistent extent computation regardless of layer organization
This design ensures robust behavior across arbitrarily complex project structures while minimizing code complexity in cli_anything/qgis/core/layouts.py.
Summary
- Layer enumeration:
project.mapLayers().values()automatically flattens nested group hierarchies - Extent computation:
_combined_project_extent()merges all layer bounds into a unifiedQgsRectangle - Positioning: Map items are placed at specified
(x, y)coordinates with millimeter precision usingQgsLayoutPoint - Fallback behavior: When no extent is provided, the engine defaults to showing all project layers
- No custom recursion: The engine relies on QGIS's native APIs rather than implementing group-tree traversal
Frequently Asked Questions
How does CLI-Anything handle layers nested five or more levels deep?
The engine handles arbitrarily deep nesting because project.mapLayers() is implemented within QGIS itself, which recursively enumerates all layer tree nodes. CLI-Anything inherits this capability without additional code—layers at any depth in the group hierarchy are included in the extent calculation just like top-level layers.
Can I exclude specific group layers from the automatic extent calculation?
Not through _combined_project_extent() directly, which collects all project layers indiscriminately. To exclude groups, you must either provide an explicit extent string via _parse_extent(), or modify the layer visibility in the QGIS project before calling the layout engine.
What units does the layout engine use for positioning map items?
All positional parameters (x, y, width, height) use millimeters as the unit of measurement, specified through QgsUnitTypes.LayoutMillimeters when constructing QgsLayoutPoint and QgsLayoutSize objects.
Does the engine recompute extents when layers are added or removed during automation?
The extent is computed at the moment add_map_item() is called. Each invocation triggers a fresh call to _combined_project_extent() (if using automatic mode), which reflects the current state of project.mapLayers(). For dynamic workflows, ensure layer modifications are committed to the project before invoking the layout engine.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →