8.1 Where things live#
src/ulg/
data/
palette.json colour tokens (family.step)
settings.json line weights, LOD breaks, ramps, class palettes, attribute docs, centre-line widths
elements/*.json the catalog, one file per theme group, keyed by element id
themes/*.json official conventions (planzv, alkis, basemap, bfn, osm, mono)
crosswalks/*.json 26 external classifications -> element ids
agent/ SKILL.md, AGENTS.md, AGENTS.snippet.md for coding agents
catalog.py loading, palette resolution, themes
crosswalk.py resolve / classify / explain
render/ renderer: geometry, texture motifs, scene, SVG and Matplotlib backends
export/ QGIS, SLD, web (MapLibre), design tokens
analysis.py indicators, flatten, root protection zones
check.py legibility report
legend.py, sheet.py legends and style sheets
brand.py the UrbanSens credit: website, logo, credit line, the small mark on the sheets
tools/ build_docs.py, build_html.py, build_logo.py, build_reference.py, format_data.py, qgis_render.py
html/ style sheet, script and font of the HTML documentation
tests/ pytest suite
docs/ the documentation in German (default); docs/en is the English version;
docs/reference and docs/html are generated, docs/img holds the figures
examples/ runnable examples and the MapLibre page
LICENSE, CITATION.cff MIT licence and the machine-readable reference
Data first. Almost every change is a change to a JSON file. Python code changes only when the style needs a new kind of mark (a motif or a pictogram) or a new capability.
8.2 Add an element#
- Check that it is missing.
ulg find "<German and English names>". Many "new" things are aliases of an existing element. Then add the alias instead. - Pick the group and file. Vegetation in
10_…/20_…, trees in30_trees.json, water, ground, surfaces, land use, built, points, lines, overlays in their files. - Write the entry with palette tokens only:
"wood_pasture": {
"group": "vegetation.woody",
"geometry": ["polygon"],
"z": 20,
"label": {"en": "Wood pasture", "de": "Hutewald / Weide mit Bäumen"},
"description": {"en": "Grazed grassland with scattered old trees. Very high biodiversity.",
"de": "Beweidetes Grünland mit locker stehenden alten Bäumen. Sehr hohe Biodiversität."},
"fill": "fallow.200",
"textures": [
{"motif": "grass_tufts", "ink": "fallow.700", "ink2": "grass.700", "keep": 0.45},
{"motif": "canopy", "ink": "leaf.800", "tones": ["leaf.400", "leaf.500", "leaf.300"],
"crown_m": 12.0, "pack": 3.4, "keep": 0.7}
],
"attributes": {"sealing": "unsealed", "bff": 1.0, "runoff_cm": 0.1, "nrr_urban_green": true, "layer": "ground"},
"aliases": ["Hutewald", "Hutung", "Waldweide", "wood pasture", "wooded pasture"]
}
- Choose
zby band (style guide 2.7): 20 for anything that covers the ground (including complexes), 30–33 for water and what lies on it, 50–66 for hedges and built structures, 66–76 for trees and points, 84–98 for overlays. - Attributes only with a source. Use the keys documented in
settings.json → attributes; leave a coefficient out rather than estimate it. A new attribute needs its own entry there (meaning,source,evidence). - Aliases in both languages, including the terms of the standards that name this thing (ALKIS, BKompV, OSM): search and agents rely on them.
- Connect it: point the crosswalk entries that describe it to the new id (8.6).
- Check and regenerate (8.8).
8.3 Add or change a colour#
Colours live only in palette.json. Stay in the tonal range of the family: light steps (100–400) for
fills, darker steps (500–900) for marks. ulg check shows whether a change brings two land covers
closer than ΔE₀₀ 10 without a texture difference, also for simulated colour-vision deficiencies.
Changing a token changes every element, theme export and sheet that uses it. That is the point, but
note it in the changelog.
8.4 Add a texture motif#
A motif is a function in src/ulg/render/motifs.py:
def my_motif(region, p, ctx: Ctx) -> list:
"""What it draws, in one line."""
x, y, s = G.scatter(region, ctx, p["spacing"], salt=p.get("salt", 31), keep=p["keep"])
...
return [Paths(...), Dots(...)]
regionis the polygon,pthe parameters merged fromDEFAULTS[motif](per LOD) and the element's texture entry,ctxcarries the scale (ctx.umetres per paper mm), the LOD and the hand-drawn factor.- Place marks with the ground-anchored helpers (
G.scatter,_span_lines, theRstreams), never with an unseeded random generator: that keeps patterns continuous across neighbouring polygons, reproducible, and seamless when drawn into tiles (ctx.period). - Sizes are in paper millimetres; convert with
ctx.u. - Register it in
MOTIFSand give itDEFAULTSfor LOD 1–3.tests/test_render_engine.pychecks that tiles repeat without seams.
8.5 Add a pictogram or a theme#
Pictograms are small vector drawings in PICTOGRAMS (src/ulg/render/scene.py): a list of parts
(kind, coordinates, fill role, stroke role, width), coordinates in units of the symbol radius, roles
fill, ink, paper, accent taken from the element's symbol.
Themes are JSON files in src/ulg/data/themes/:
| Key | Meaning |
|---|---|
title, description, source, evidence |
what the convention is and where the values come from |
handdrawn |
outline wobble for this theme (0 = exact lines) |
textures |
"none" (flat fills; overlays keep their hatches) or "mono" (every mark in ink) |
background, ink, paper |
page colours |
default, groups, elements |
fill/outline overrides, from general to specific; each with an evidence note |
A new theme appears in ulg.themes(), the CLI --theme options and all exporters automatically.
8.6 Add or extend a crosswalk#
One file per scheme in src/ulg/data/crosswalks/; the format is in
reference/crosswalk-format.md. Essentials:
- metadata:
title,publisher,version,source,license_note,key,fields(column aliases), optionalhierarchy,suffixes,fallback_pattern; - one entry per class with
codeormatch,name,element(ornull),fit, andcoloronly if the scheme's own legend colour was verified; - the most specific match wins, then file order.
python -m pytest tests/test_crosswalks.py validates every file. Add spot checks for the lookups your
project relies on.
8.7 Tools#
| Command | Does |
|---|---|
python tools/format_data.py |
normalises the layout of all JSON data files (--check only reports) |
python tools/build_reference.py |
regenerates docs/reference/*.md from the data and docstrings |
python tools/build_docs.py [names] |
regenerates the images in docs/img/, in both languages (sheets, textures, maps, QGIS renders, the banners of the HTML pages) |
python tools/build_logo.py |
redraws the logo, its variants and the favicons in docs/img/logo/ from the catalog |
python tools/build_html.py |
builds docs/html/: the German site (root) and the English site (en/), each with a single file (banner, serif text, numbered figures, quotations with their source); checks every link |
python -m pytest |
the test suite (-m "not slow" skips sprite rasterisation) |
ulg check |
the legibility report |
8.8 Checklist for every change#
python tools/format_data.py
ulg check
python -m pytest
python tools/build_reference.py
python tools/build_docs.py sheets
python tools/build_html.py
Then look at the regenerated sheets: the style is visual, and the sheets are its review copy. A change to the text of the documentation is made in both languages (8.11).
8.9 Versions and the living style guide#
The style is versioned with the package (pyproject.toml, ulg.__version__ and palette.json → version move together) and every release is described in CHANGELOG.md:
| Change | Version step |
|---|---|
| an element id removed or renamed, or its meaning changed | major |
| new elements, themes, crosswalks, motifs; visible colour or texture changes | minor |
| fixes that do not change how existing maps look | patch |
Proposals for the style work best as a picture: a crop of the sheet or a map with the problem, the
element ids involved, and, for anything official, the source. Apps pick up a release through
ulg export tokens / ulg export web; QGIS projects by reloading the exported QML files; agents
through the installed skill, which reads the installed package.
8.10 Publishing#
Two GitHub workflows in .github/workflows/ run on every push to main:
| Workflow | Does |
|---|---|
ci.yml (Tests) |
installs the package on Python 3.10, 3.12 and 3.13, checks the data layout, runs ulg check and the test suite |
pages.yml (Documentation) |
builds both sites and their single files with python tools/build_html.py --out _site (it fails on a broken link), exports and copies the MapLibre demo, and publishes everything with GitHub Pages at https://urbansens.github.io/Urban-Landscape-Graphics/ (German) and https://urbansens.github.io/Urban-Landscape-Graphics/en/ (English) |
Generated files are not committed: docs/html/, examples/output/ and the exported web style in
examples/web/ulg/ are rebuilt by the workflows or by the commands in 8.7. What is committed are the
images in docs/img/ (sheets, maps, banners, logos), because they are the review copy of the style.
Regenerate them with python tools/build_docs.py and python tools/build_logo.py before committing a
change that alters how the style looks. The GitHub social preview (1280 × 640 px, set by hand under
Settings > General) is docs/img/social-preview.png (German) or social-preview-en.png, made by
python tools/build_docs.py social.
8.11 Two languages#
The documentation exists in German and English, and German is the default:
| German (default) | English | |
|---|---|---|
| Start page | README.md |
README.en.md |
| Chapters | docs/index.md, docs/01-origins.md ... docs/08-extending.md, docs/licence-and-credit.md |
the same file names in docs/en/ |
| Figures with text | docs/img/name-de.png |
docs/img/name.png |
| Published | root of the site | /en/ |
The reference pages, the research files, the examples, the changelog and AGENTS.md are English only; both sites
contain them, and the German menu marks them with EN. The German text uses the formal Sie, and it is translated
for meaning and idiom rather than word by word. Keep file names, heading numbers, figures and the number of
code blocks identical in both languages; tests/test_docs_i18n.py compares the two trees. Text in figures comes from
tools/build_docs.py, which draws every language-dependent image twice. The language switch of the site needs
nothing: the builder pairs the pages by file name.
Two house rules apply to all text, in both languages: no em dashes (write a comma, a colon, parentheses or a
full stop) and no spaced en dashes in their place. tests/test_style_rules.py checks this.