Land Cover¶
Toolkit name: GIS_LandCover (constant toolkitHome.GIS_LANDCOVER)
Class: hera.measurements.GIS.raster.landcover.LandCoverToolkit (internal toolkit name LandCoverToolkit)
Reads a land-cover classification raster (e.g. MODIS MCD12Q1, IGBP classes) and converts land-cover classes to aerodynamic roughness length (z0) for atmospheric and CFD boundary conditions. Urban cells can instead get z0 from building morphology through the Buildings toolkit.
from hera import toolkitHome
# Tip: if you created the project with `hera-project project create`, you can omit projectName
lc = toolkitHome.getToolkit(toolkitHome.GIS_LANDCOVER, projectName="MY_PROJECT")
cls = lc.getLandCoverAtPoint(lon=35.0, lat=32.0) # int class
z0 = lc.getRoughnessAtPoint(lon=35.0, lat=32.0) # float, m
For the full API, see the API Reference.
Data source format¶
| Property | Value |
|---|---|
| File format | Single-band GeoTIFF (band 1), UINT8 class values 0–16 |
| Classes (IGBP / MCD12Q1 Type 1) | 0 Water, 1–5 Forests, 6–7 Shrublands, 8–9 Savannas, 10 Grasslands, 11 Permanent wetlands, 12 Croplands, 13 Urban and built-up, 14 Cropland/natural vegetation mosaic, 15 Snow and ice, 16 Barren |
| CRS | Read from the GeoTIFF; point coordinates are reprojected to it. The grid methods assume a WGS84 raster. |
dataFormat |
"string" (the path to the .tif, opened with rasterio) is recommended. "geotiff" also works but needs the GDAL Python bindings. |
desc.type |
The classification scheme, used to pick the roughness table. Set it to 1 (see Roughness tables). |
| Config key | defaultLandCover — the datasource used when dataSourceName is not given. It must be set, otherwise the calls raise KeyError. |
Repository JSON example:
{
"GIS_LandCover": {
"Config": { "defaultLandCover": "Type-1" },
"DataSource": {
"Type-1": {
"isRelativePath": "True",
"item": {
"resource": "data/landcover/lc_mcd12q1.tif",
"dataFormat": "string",
"desc": { "year": 2021, "type": 1 }
}
}
}
}
}
Coordinate convention¶
getLandCover and getRoughness take a bounding box minx, miny, maxx, maxy and a grid spacing dxdy in meters (the grid is built in ITM and converted to WGS84).
- With
inputCRS=WSG84(the default),minx/maxxare latitudes andminy/maxyare longitudes. - With
inputCRS=ITM, they are eastings and northings.
The point methods take lon, lat in that order.
Land cover¶
from hera.measurements.GIS import WSG84
# Class at a point (raises IndexError outside the raster)
cls = lc.getLandCoverAtPoint(lon=35.0, lat=32.0)
# Class grid: minx/maxx = latitude, miny/maxy = longitude
grid = lc.getLandCover(minx=31.9, miny=34.9, maxx=32.1, maxy=35.1, dxdy=500, inputCRS=WSG84)
grid["landcover"] # (i, j) coordinate, alongside 'lat', 'lon' and the scalar 'dxdy'
# Class names
names = lc.getCodingMap("Type-1") # {0: "Water", 1: "Evergreen needleleaf forest", ...}
| Method | Returns |
|---|---|
getLandCoverAtPoint(lon, lat, inputCRS=WSG84, dataSourceName=None) |
int class value |
getLandCover(minx, miny, maxx, maxy, dxdy=30, inputCRS=WSG84, dataSourceName=None) |
xarray.DataArray over (i, j) with lat, lon, dxdy and landcover coordinates |
getCodingMap(datasourceName) |
Class-to-name dict. Only the literal argument "Type-1" is recognised; anything else returns {}. |
dataSourceName may also be a path to a .tif file instead of a registered datasource name.
Roughness¶
# z0 grid from a bounding box (getLandCover + getRoughnessFromLandcover)
rough = lc.getRoughness(minx=31.9, miny=34.9, maxx=32.1, maxy=35.1, dxdy=500)
rough["z0"]
# Or add z0 to a grid you already have
rough = lc.getRoughnessFromLandcover(grid)
# Equivalent sand-grain roughness Ks = 30 * z0 (for OpenFOAM nutkRoughWallFunction)
ks = lc.roughnesslength2sandgrainroughness(0.1)
| Method | Description |
|---|---|
getRoughnessAtPoint(lon, lat, inputCRS=WSG84, dataSourceName=None) |
z0 (float) at a point |
getRoughness(minx, miny, maxx, maxy, dxdy=30, inputCRS=WSG84, dataSourceName=None, isBuilding=False, windMeteorologicalDirection=None, resolution=None, GIS_BUILDINGS_dataSourceName=None) |
Land-cover grid with a z0 coordinate |
getRoughnessFromLandcover(landcover, windMeteorologicalDirection=None, resolution=None, isBuilding=False, dataSourceName=None, GIS_BUILDINGS_dataSourceName=None) |
Adds z0 to a getLandCover result |
roughnesslength2sandgrainroughness(rl) |
Static: returns rl * 30 |
Roughness tables¶
The table is chosen from the datasource desc.type:
type: 1uses the MCD12Q1 Type-1 table from Floors et al. (2021), e.g. water 0.0001 m, forests 1 m, croplands 0.15 m, urban 0.8 m. Both the point and the grid methods use it.- Without a
type,getRoughnessAtPointfalls back to an illustrative placeholder table (0.01–1.4 m, increasing with the class number), whilegetRoughnessFromLandcover/getRoughnessraiseAttributeError. Always settype: 1for MCD12Q1 data.
Urban areas from buildings¶
With isBuilding=True, z0 is derived from building morphology instead of the class table. The toolkit queries the Buildings toolkit of the same project over the grid area, runs LambdaFromBuildingData(windMeteorologicalDirection, resolution, buildings) and computes z0 and displacement height from λp, λf and hc.
rough = lc.getRoughness(
minx=32.05, miny=34.75, maxx=32.10, maxy=34.80, dxdy=100,
isBuilding=True,
windMeteorologicalDirection=270, # meteorological degrees
resolution=200, # m, block size for the lambda calculation
GIS_BUILDINGS_dataSourceName="BNTL",
)
# adds 'z0', 'dd', 'lambdaF', 'lambdaP', 'hc', 'll'
windMeteorologicalDirection and resolution are required with isBuilding=True (otherwise ValueError), and an empty building query raises ValueError.
Known issue
For cells with no buildings, the urban path stores the land-cover class (from getLandCoverAtPoint) in z0 instead of a roughness (hera/measurements/GIS/raster/landcover.py:673).
Presentation layer¶
lc.presentation draws the grid cells as coloured squares over a background image. plot must be a matplotlib AxesImage in ITM coordinates (for example ax.get_images()[0] after plotting a map from the Tiles toolkit). Each method opens a new figure and calls plt.show(); nothing is returned.
| Method | Description |
|---|---|
plotLandcover(plot, landcover, alpha=0.2, figsize=(28, 28)) |
Land-cover classes in fixed colours |
plotRoughness(plot, landcover, alpha=0.5, figsize=(28, 28)) |
z0 with a viridis colour bar |
plotLambdas(field, plot, landcover, alpha=0.2, figsize=(28, 28)) |
One of the urban fields ("lambdaF", "lambdaP", "hc", ...) |
Command line¶
hera-GIS landcover getLandcover --minx 31.9 --miny 34.9 --maxx 32.1 --maxy 35.1 \
--dxdy 500 --filePath landcover.csv --projectName MY_PROJECT
Writes a CSV with lat, lon, landcover and (by default) z0.
Known issue
In master the bounding-box options are passed to the toolkit as strings, --roughness/--isBuilding are type=bool (any value, even False, means True), and --windDirection hits a typo (arguments.windDirectionis) in hera/measurements/GIS/CLI.py. Prefer the Python API.
Complete example¶
from hera import toolkitHome
lc = toolkitHome.getToolkit(toolkitHome.GIS_LANDCOVER, projectName="MY_PROJECT")
# Register the raster once
lc.addDataSource("Type-1", resource="/data/landcover/lc_mcd12q1.tif", dataFormat="string",
version=(0, 0, 1), desc={"type": 1})
lc.setDataSourceDefaultVersion("Type-1", (0, 0, 1))
lc.setConfig(defaultLandCover="Type-1")
# Roughness for a 20 km x 20 km area at 500 m resolution (lat first for WGS84)
rough = lc.getRoughness(minx=31.9, miny=34.9, maxx=32.1, maxy=35.1, dxdy=500)
import pandas as pd
df = pd.DataFrame({f: rough[f].values.ravel() for f in ["lat", "lon", "landcover", "z0"]})
df.to_parquet("roughness.parquet")