This is an old revision of the document!


PHP's gd library is missing or unable to create PNG images

Working with Subregions Using Region Manager

Overview

Some workflows shouldn't run against a whole map at once — a national land-use model might need different parameters per country, or a hydrology model might only make sense computed watershed by watershed. Region Manager is what makes that practical: define the regions once, from a categorical map whose class values are the boundaries, and everything downstream — reading metadata, narrowing to one region, splitting a map into pieces, and merging the pieces back — works against that single definition.

The running example through this page computes an accumulated-cost surface separately for each region, using Calc Cost Map, then reassembles the per-region results into one global cost map. It's a genuinely representative case: split, do real per-region work, collect, merge.

Defining regions

Region Manager is the container everything else on this page depends on.

Port Direction Type Required? Description
regions Input Categorical Map Yes Map of subregions, e.g., countries, states, counties, etc.
borderCells Input Non Negative Integer Value No, default 0 Number of lines and columns used to create an additional border around each region.
regionManager Internal Output Region Manager The region manager, exposed to functors nested inside the container.

A nonzero borderCells matters for anything that accumulates across neighboring cells — a cost surface, a neighborhood statistic — since without it, a region's edge cells would be computed as if the world stopped there.

Example — the shell everything else in this page builds on:

sourcesMap := LoadMap "c:/data/sources.tif";
frictionsMap := LoadMap "c:/data/frictions.tif";
regionsMap := LoadCategoricalMap "c:/data/regions.tif" .none .default 0;

RegionManager regionsMap 5 {{
    // everything below is added here, one section at a time
}};

Reading region metadata

Get All Regions Info returns boundary details for every region at once, as a table — the natural thing to feed a loop:

Port Direction Type Required? Description
regionManager Input Region Manager No, auto-bound The region manager to read from.
regionsInfo Output Table One row per region: RegionId*, RegionLines, RegionColumns, RegionTopYCoordinate, RegionLeftXCoordinate, RegionBottomYCoordinate, RegionRightXCoordinate.

Get Region Info is the single-region equivalent, useful when you need more than a bounding box for one particular region — it also returns a mask isolating that region and its coordinate reference system:

Port Direction Type Required? Description
regionId Input Integer Value No, auto-bound Which region to describe.
regionManager Input Region Manager No, auto-bound The region manager to read from.
regionMask Output Categorical Map A map isolating just this region.
regionLines Output Positive Integer Value The region's height, in cells.
regionColumns Output Positive Integer Value The region's width, in cells.
regionProjection Output Projection The coordinate reference system of the source map.
regionTopYCoordinate, regionLeftXCoordinate, regionBottomYCoordinate, regionRightXCoordinate Output Real Value The region's four coordinate boundaries.

Add the enumeration step to the running example:

RegionManager regionsMap 5 {{
    allRegions := GetAllRegionsInfo;
}};

Visiting one region, or every region

Region narrows scope to a single region for whatever's nested inside it:

Port Direction Type Required? Description
regionId Input Integer Value No, auto-bound (editable) Which region to select.
regionManager Input Region Manager No, auto-bound The region manager to select from.
regionId Internal Output Integer Value Re-exposed to functors nested inside.
regionManager Internal Output Region Manager Re-exposed to functors nested inside.

Visiting every region means reading Get All Regions Info's table and looping over it with For Each, nesting Region inside to re-expose the loop's current key under the name regionIdRegionalize Map and the rest of this page's functors auto-bind to a port by that exact name, not to For Each's own step.

It's tempting to reach for For Each Region instead, since it bundles “define regions” and “loop over them” into one container. Resist that here: it has its own required regions input and creates its own region manager internally, rather than sharing one from an enclosing Region Manager. That's fine if the loop is the entire task, but this page's example needs the *same* region manager instance still available after the loop, for the merge — which For Each Region can't provide, since its manager stops existing once its own block ends.

RegionManager regionsMap 5 {{
    allRegions := GetAllRegionsInfo;

    _ := ForEach allRegions {{
        Region step {{
            // regionId comes from step; regionManager auto-binds from the
            // enclosing RegionManager.
        }};
    }};
}};

Splitting a global map into regions

Regionalize Map and Regionalize Categorical Map cut a global map down to one region's extent:

Port Direction Type Required? Description
globalMap Input Map / Categorical Map Yes The map to split.
regionId Input Integer Value No, auto-bound Which region to extract.
keepNonRegionCells Input Boolean No, default No If true, cells outside the region mask (including border cells) are kept rather than set to null.
recreateCategories Input Boolean No, default No — Regionalize Categorical Map only If true, the region's category set is recomputed from the values actually present in it. If false, the full category set from the global map is kept, even categories absent from this particular region.
regionManager Input Region Manager No, auto-bound The region manager defining the split.
regionalMap Output Map / Categorical Map The map cut down to this region.

This is where the actual per-region computation happens — split the global inputs, run whatever the model needs against the region-sized pieces, in this case Calc Cost Map:

Region step {{
    regionalSources := RegionalizeMap sourcesMap;
    regionalFrictions := RegionalizeMap frictionsMap;

    { costs = regionalCost, directions = _ } :=
        CalcCostMap regionalSources regionalFrictions .no;
}};

Collecting regional results

Regional Map and Regional Categorical Map tag a per-region fragment under a shared name as it's produced — neither has an output; they're pure collectors, accumulating fragments as a side effect of the loop running:

Port Direction Type Required? Description
globalMapName Input Name Yes The shared name this regional fragment will be collected under.
regionalMap Input Map (Regional Map) / Categorical Map (Regional Categorical Map) Yes The per-region result to store.
regionId Input Integer Value No, auto-bound Which region this fragment belongs to.
regionManager Input Region Manager No, auto-bound The region manager this fragment belongs to.
Region step {{
    regionalSources := RegionalizeMap sourcesMap;
    regionalFrictions := RegionalizeMap frictionsMap;

    { costs = regionalCost, directions = _ } :=
        CalcCostMap regionalSources regionalFrictions .no;

    RegionalMap "cost_map" regionalCost;
}};

Merging regions back into one map

Merge Regional Maps and Merge Regional Categorical Maps reassemble everything stored under a shared name back into one global mosaic:

Port Direction Type Required? Description
globalMapName Input Name Yes The name the fragments were collected under, via Regional Map or Regional Categorical Map.
mergeNonRegionCells Input Boolean No, default No If true, cells outside the region mask (including border cells) are merged in too, rather than ignored. Two overlapping cells can only be combined if they share the same value or one of them is null.
regionManager Input Region Manager No, auto-bound The region manager the fragments belong to.
globalMap Output Map / Categorical Map The reassembled mosaic.

There's a trap here worth naming: neither merge functor has a sequenceInput of its own, and nothing about globalMapName — just a literal string — creates a data dependency on the loop that populated it. Left as-is, the engine has no reason to run the merge *after* the loop rather than before or during it. For Each does expose a sequenceOutput once the whole loop finishes, but with nowhere on the merge functor to plug it into, the fix is a Group wrapped around the merge, whose own sequenceInput the loop's sequenceOutput connects into.

A few more things worth knowing before relying on the merge:

  • The merged map's format is inherited from the first fragment stored, not negotiated across all of them. Cell type, null value, and categorization all come from whichever Regional Map or Regional Categorical Map call registered first for that name — only the dimensions come from the regions map itself. If regions are processed in an order that isn't guaranteed, don't rely on which fragment “wins.”
  • Mismatched null values fail the merge, not silently overwrite each other. When mergeNonRegionCells brings overlapping border cells from two different regions together, they can only combine if they agree, or one of them is null — two regions disagreeing on what a cell's value should be raises an error rather than picking one.
  • A layer's sparse storage survives the merge, so long as every regional fragment for that layer used it too — the merged global layer comes out sparse rather than being densified. See Map Type's Storage section for what sparse storage means.
  • Fragments are consumed, not just read. Once the merge for a given globalMapName runs, the manager no longer holds those fragments — there's no merging the same name twice, and nothing left to read back under it afterward.

Relaying a Region Manager between functors

Region Manager Value exists purely to pass a Region Manager value through — the same relay role Struct's carrier functor plays for structs. Its own input auto-binds to the enclosing container's regionManager by default and cannot be given as a constant.

Putting it together

sourcesMap := LoadMap "c:/data/sources.tif";
frictionsMap := LoadMap "c:/data/frictions.tif";
regionsMap := LoadCategoricalMap "c:/data/regions.tif" .none .default 0;

RegionManager regionsMap 5 {{
    allRegions := GetAllRegionsInfo;

    sequenceOut := ForEach allRegions {{
        Region step {{
            regionalSources := RegionalizeMap sourcesMap;
            regionalFrictions := RegionalizeMap frictionsMap;

            { costs = regionalCost, directions = _ } :=
                CalcCostMap regionalSources regionalFrictions .no;

            RegionalMap "cost_map" regionalCost;
        }};
    }};

    _ := Group sequenceOut {{
        costMap := MergeRegionalMaps "cost_map" .no;
        SaveMap costMap "c:/data/cost_map_merged.tif";
    }};
}};

One region manager, defined once at the top, is what threads the whole thing together: Get All Regions Info reads from it, Region narrows against it once per loop iteration, Regional Map stores fragments into it, and Merge Regional Maps — deliberately delayed until the loop's sequenceOutput fires — reads every one of those fragments back out of the same instance.