Documentation

Pro handbook

Customizing the look

Every drawn state — normal, hover, selected, preview and (for faces) invalid — comes from a fixed set of style tables in @vectorstream/adapter (darstellung.ts): FLAECHE, KANTE, GRIFF, MITTELPUNKT, PUNKT, HILFSLINIE, STEUERGRIFF, plus the label tables BESCHRIFTUNG_FARBE and BESCHRIFTUNG_GROESSE, and the single-object BESCHRIFTUNG_SCHRIFT/BESCHRIFTUNG_KASTEN. Until this feature, all three map adapters imported these constants directly — there was no published way to change a color or a line width for an embedding application's own corporate design.

The darstellung option, present on LeafletAdapterOptions, OpenLayersAdapterOptions and MapLibreAdapterOptions, fixes that: a partial override, merged with the built-in defaults per field, set once when the adapter is built.

The override: per field, not per whole table#

An override names only what actually changes. Every field it leaves out — on a state it doesn't mention, or on a field of a state it does — keeps the built-in default:

import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { baueDarstellung, type DarstellungUeberschreibung } from '@vectorstream/adapter';
import { LeafletAdapter } from '@vectorstream/leaflet';

/**
 * A corporate-design override, the whole feature in one example (Lücke 3,
 * "Darstellung/Stil-Schnittstelle von außen"): only the fields that actually change from the
 * built-in defaults — a brand color for faces and edges in their resting state. Everything left
 * out (line width, opacity, every other state, every other table) keeps its default;
 * `LeafletAdapter`/`OpenLayersAdapter`/`MapLibreAdapter` all merge this into the full set of style
 * tables once, at construction, through the same `baueDarstellung` — never again afterward.
 */
export function markenDarstellung(): DarstellungUeberschreibung {
  return {
    flaechen: { normal: { farbe: '#7a1fa2', fuellung: '#7a1fa2' } },
    kanten: { normal: { farbe: '#7a1fa2' } },
  };
}

/** A Leaflet map whose faces and edges are drawn in the brand color above; every other state (hover, selected, preview, invalid) keeps its built-in appearance. */
export function starteMarkenKarte(container: HTMLElement): { map: L.Map; adapter: LeafletAdapter } {
  const map = L.map(container).setView([51.5112, 7.4555], 17);
  const adapter = new LeafletAdapter(map, { darstellung: markenDarstellung() });
  return { map, adapter };
}

/**
 * An unknown key — here a misspelled state — is rejected right at construction, in the current
 * locale, instead of being silently ignored (then the map would just show the default, and
 * nobody would know why). The message carries both the wrong path and the valid alternatives.
 */
export function pruefeTippfehler(): string {
  try {
    baueDarstellung({ flaechen: { normall: { farbe: '#7a1fa2' } } } as unknown as DarstellungUeberschreibung);
    return 'no error';
  } catch (fehler) {
    return fehler instanceof Error ? fehler.message : 'unknown error';
  }
}

In this example, flaechen.normal.breite (the outline width), flaechen.hover/selected/ preview/invalid, and every table besides flaechen/kanten are entirely untouched — the map still shows the built-in yellow hover, orange selection and so on, just with the brand color where nothing else was said.

What can be overridden#

One group per style table, each named after the table it overrides:

DarstellungUeberschreibung field Default table Keyed by
flaechen FLAECHE FaceState (normal, hover, selected, preview, invalid)
kanten KANTE EdgeState (normal, hover, selected, preview)
griffe GRIFF HandleState (normal, hover, selected, active) — node handles
mittelpunkte MITTELPUNKT HandleState — edge-midpoint handles
steuergriffe STEUERGRIFF move/radius/other — plain-circle control handles
punkte PUNKT HandleState | 'preview' — free-standing points drawn as a circle
hilfslinien HILFSLINIE standard/auswahl/zeichnung/rasthilfe — guide-line roles
beschriftungFarbe BESCHRIFTUNG_FARBE label state → font color
beschriftungGroesse BESCHRIFTUNG_GROESSE size level → pixels
beschriftungSchrift BESCHRIFTUNG_SCHRIFT one object: familie, zeilenhoehe
beschriftungKasten BESCHRIFTUNG_KASTEN one object: hintergrund, polsterX, polsterY, eckenradius

invalid is already covered today (flaechen.invalid) — the only state that exists on any table right now. A planned Geofencing task adds further invalid states to EdgeState and ScenePoint; each override group's type (TeilUeberschreibung<typeof FLAECHE> and so on) is derived straight from its table, so the moment KANTE/PUNKT gain that key, kanten.invalid/ punkte.invalid become overridable automatically — no change to this feature needed. Guide lines are the one exception to that automatic story: SceneGuide has no state field at all — hilfslinien's keys are its four fixed roles (standard, auswahl, zeichnung, rasthilfe), not a state. An "invalid" guide line would need a new role or a new field on SceneGuide first — a design decision Geofencing hasn't made yet — and a matching change to hilfslinienStil itself, not merely a table key filling in on its own.

Not covered, by design: the icon-based symbols — the four snap markers, the rotate/scale control handles' own icon, the pin-shaped point's fixed color. These are already deliberately distinguished by shape, not color alone (so a color-blind user can still tell a corner snap from a midpoint snap); branding them would undercut that. Everything drawn as a plain circle or line is overridable; everything drawn as a fixed symbol is not. Concretely, this is where the exception is visible: overriding griffe.hover still leaves the rotate/scale handles' own hover color yellow (STEUER_FARBE), overriding hilfslinien.rasthilfe still leaves the four snap markers magenta (SNAP_SVG), and overriding punkte.normal still leaves a pin-shaped point blue (PIN_FARBE) — only the circle shape of a point follows punkte. The label plate's drop shadow, its glow blur, and a dashed guide line's own dash pattern (HILFSLINIE_GESTRICHELT, distinct from SceneGuide.style) are fixed the same way, for the same reason: they're a small amount of hard-coded polish, not a themable color or line width.

A circle handle's hit area grows with an enlarged radius. Overriding griffe.normal.radius (or mittelpunkte/steuergriffe) to something larger than the built-in default also enlarges how far from its center a click still grabs it — otherwise a visually bigger handle would promise more than it can actually be clicked on. It never shrinks the hit area below the ordinary pixel tolerance, even for a radius overridden smaller than the default. The icon-based handles (rotate, scale) are unaffected, consistent with them not being themable at all.

Precedence: an explicit override always wins over a layer color#

An application can color faces and edges two ways at once: the already-existing ebenenFarbe adapter option (see Layers) tints every normal-state element that belongs to a layer, and darstellung.flaechen.normal/darstellung.kanten.normal sets a fixed color regardless of layer. When both are in play for the same field, the explicit darstellung override wins — a layer tint never paints back over a color the application named itself. This applies field by field: naming only farbe in the override still lets the layer color tint fuellung, and vice versa. Without this rule, an application using both options at once would see its brand color silently disappear on every layered element, with no error to explain why — the same "nothing happened" a typo check elsewhere in this feature exists to prevent, just reached by a different route.

One merge function, shared by all three adapters#

mitUeberschreibung (@vectorstream/adapter) is the single implementation of "partial, per field": given a default table and an override, a key the override doesn't mention keeps the default's value by reference, and a key it does mention replaces only the fields it names, via a copy that never touches the original. Neither the default table nor the override object is ever mutated — that, not the sharing of references as such, is what lets two adapters each hold a different override of the same table at the same time without influencing one another, or the built-in default either (checked in darstellung.test.ts). baueDarstellung calls it once per table and bundles the result into a Darstellung; LeafletAdapter, OpenLayersAdapter and MapLibreAdapter each do exactly

this.darstellung = baueDarstellung(options.darstellung);

in their constructor, and nowhere else — there is no method to change it later (see "When it applies" below).

Dash patterns: overridable on every state the type allows#

strich (the dash pattern) is a field of StrichStil, so it can be named on any state of flaechen/kanten/griffe/mittelpunkte/steuergriffe/punkte — not only the states whose built-in default happens to already have one (today, only preview). A dashed kanten.normal — an ordinary corporate-design choice — works even though KANTE.normal itself has no dash pattern to start from:

baueDarstellung({ kanten: { normal: { strich: [4, 2] } } });

hilfslinien is the one exception, and still correctly rejected: its style type (Omit<StrichStil, 'strich'>) never had a dash pattern to begin with — a guide line's dashing always comes solely from SceneGuide.style, never from its own table (see HILFSLINIE's own doc comment) — so naming strich there is an unknown field, not an omission to fix.

To force a solid line on a state whose default is dashed, override strich with an empty array (strich: []), not undefined — an explicit undefined counts as "not provided" and leaves the default's dash pattern (or lack of one) exactly as it was (see "Nothing is silently blanked out" below). This is checked on all three renderers and the canvas export: an empty strich draws solid everywhere, never an invisible line — MapLibre's own line layer once needed a dedicated fix for this (linienWerte, fix round 2), since its dash expression only falls back to the solid pattern when the property is entirely absent, and a present-but-empty array isn't.

Validation: a typo fails loudly#

An override that names a group, a state/role/level within a group, or a field that doesn't exist on the corresponding style type throws right at baueDarstellung — i.e. at adapter construction — instead of being silently ignored. The same applies to a value that should be a plain object (the override itself, a group, or a state) but isn't — a string, null, an array:

import { baueDarstellung, type DarstellungUeberschreibung } from '@vectorstream/adapter';

// Cast needed only to get the typo past the compiler for this example — without the cast,
// TypeScript already rejects `normall` as not a property of `DarstellungUeberschreibung['flaechen']`,
// which is exactly the point: most typos never reach `baueDarstellung` at runtime at all.
baueDarstellung({ flaechen: { normall: { farbe: '#7a1fa2' } } } as unknown as DarstellungUeberschreibung);
// throws a DarstellungFehler, code 'adapter.darstellungUnbekannterSchluessel':
// "Unknown key 'flaechen.normall' in the display override — valid: normal, hover, selected, preview, invalid."

The message is in English or German depending on the current locale (setLocale, see Language), and the error carries the usual code/params (see Validity and rejections) for an application that wants to react to it programmatically rather than show the text as is.

Nothing is silently blanked out#

A field named with the value undefined — easy to get from { farbe: theme.primary } when theme.primary happens to be absent — counts as "not provided" and never erases the default; a naive merge would otherwise set that field to undefined, since object spread copies every key regardless of its value. The same rule applies one level up: a state/role/level named with the value undefined is likewise left untouched. And an override's own array field (a strich pattern) is copied when it's merged in, not kept by reference — mutating the array afterward can't reach back into the already-built Darstellung.

Values are not otherwise checked#

Validation only checks the override's shape — which groups, states and fields exist — never the content of a value. beschriftungSchrift.familie and beschriftungKasten.hintergrund reach cssText unchecked, the same way a font family or a background color would in any hand-written stylesheet; farbe/fuellung fields reach canvas and renderer color APIs the same way. This is safe for a value the application itself chose (its own brand color, its own font stack) but not a place to pass through untrusted input — unlike SceneLabel.farbe, which comes from scene data (potentially an imported GeoJSON file) and is therefore strictly checked against #rgb/#rrggbb before use (see istGueltigeFarbe), a darstellung override is written by the application's own code, once, at construction, and is trusted the same way ebenenFarbe or any other adapter option already is.

When it applies#

The override is read once, in the constructor — not a live setting. There is no setDarstellung() and no way to change it after the adapter is built (user decision 12.10.2026): rebuilding the adapter is the way to switch the appearance, the same way ebenenFarbe already works. A future dark-mode toggle, if ever added, would do so by tearing down and rebuilding the adapter with a new darstellung option — not by mutating a live one.

Image export honors it too#

adapter.exportImage() (see Exporting the map view as an image) draws from the same resolved Darstellung the map itself uses — zeichneFlaechenAufCanvas, zeichneKantenAufCanvas, zeichnePunkteAufCanvas and zeichneBeschriftungenAufCanvas (the functions bildexport.ts exports) all take the adapter's own this.darstellung as their last argument. A corporate-design override therefore shows up in the exported PNG exactly as it shows up on screen, on every renderer — there is no second place to apply it.

See it on a map.