Labels and attributes
Attributes#
Every element — face, edge, node — can carry an attribute set: a flat object of string,
number, boolean or null values. Attributes come in through GeoJSON properties on import and
go out through them on export (see GeoJSON). They are stored next to the topology,
changed only by operations, and therefore covered by undo.
Read them with attributeVon(partition, id) from @vectorstream/core; it returns an empty object
for an element without attributes.
Any string is a valid key, including __proto__, constructor, prototype and toString. The
sets are objects with a null prototype, so such a key is an ordinary attribute, a set has no
inherited members ('constructor' in set is false), and no attribute can change
Object.prototype. Use Object.keys, Object.entries, spread or Object.fromEntries to read or copy a set (not Object.assign({}, set), which loses a __proto__ key; and not set.hasOwnProperty(...) or String(set), which throw: use Object.hasOwn or hatSchluessel); build your own
records with neuerAttributsatz() and setzeSchluessel(satz, key, value) rather than
{} and satz[key] = value.
What happens to attributes when geometry changes:
- Split (a line, chord or outline divides a face): both pieces keep the face's attributes —
except the label
textand its three style keys, which name one specific object and are dropped on both pieces. - Move to another layer: see Layers (concept) — parts from two sources get both sets, the upper layer wins conflicts, and the label survives only an unsplit move.
- Copy and paste with
editor.kopieren()/editor.einfuegen(): the copies get the attributes of the originals — for a face with a hole or an enclosed point,kopieren()rejects instead (see Modes: Pasting), so there is no copy with any attributes to get.
The editor has no general "set attribute" method. It writes the label keys described below; other attributes come from your data.
Labels#
A label is the attribute text. Faces show it at their centroid; nodes show it at their position —
a free-standing point with a label is drawn as a text on a white plate, without the point's
circle (a pin keeps its pin). Edge attributes are not drawn.
The style is three more attributes, all optional:
| Attribute | Values | Default when absent |
|---|---|---|
textGroesse |
'klein', 'gross', 'sehr-gross' |
'mittel' (medium) — never written |
textFarbe |
a CSS color | the map renderer's choice |
textFett |
'true' (or true) |
not bold |
Write them through the editor:
import { attributeVon, type Attributsatz } from '@vectorstream/core';
import type { Editor } from '@vectorstream/editor';
/** Labels a face (or node) of the active layer and styles the label — two undo steps. */
export function labelElement(editor: Editor, id: string, text: string): boolean {
// Writes the `text` attribute; an empty string removes the whole label, style included.
const named = editor.benenneElement([id], text);
// Size, color, bold. Only already-labeled elements are styled; `undefined` removes a value.
editor.gestalteText([id], { groesse: 'gross', farbe: '#b00020', fett: 'true' });
return named;
}
/** All attributes of an element: the imported GeoJSON properties plus the label keys. */
export function attributesOf(editor: Editor, id: string): Attributsatz {
return attributeVon(editor.partition, id);
}
editor.benenneElement(ids, text, layer?)setstexton all given elements in one transaction (rename), trimmed. An empty text removes the whole label —textand the three style keys — the same as an empty text in the text tool.editor.gestalteText(ids, { groesse, farbe, fett }, layer?)changes the style of elements that already have a label (style-label); unlabeled elements are skipped. A key given asundefinedremoves that style value; a key left out is not touched.- Both write only what differs: the same text again reports
unchanged, not a new undo step. layer(anEbenenId) addresses another layer than the active one.
Its test labels a parcel, finds the label with its style in the drawn scene and in the export,
splits the parcel with a line — both halves keep parcel and use, neither keeps the label — and
removes another label with an empty text.
The text tool#
The text tool labels faces and places free texts by clicking (see Modes).
It needs an input field from your application: the mode knows where and how the text is being
edited, but it draws no field. A complete field:
import type { Adapter } from '@vectorstream/adapter';
import { TextMode, type Editor } from '@vectorstream/editor';
/**
* The input field for the text tool. The mode says *that* and *where* someone is writing
* (`bearbeitung()`); the field itself is yours. `parent` is a positioned element over the map.
*/
export function attachTextField(editor: Editor, adapter: Adapter, parent: HTMLElement): HTMLInputElement {
const field = document.createElement('input');
field.type = 'text';
field.hidden = true;
field.style.position = 'absolute';
parent.append(field);
const textMode = (): TextMode | undefined => (editor.mode instanceof TextMode ? editor.mode : undefined);
const update = (): void => {
const editing = textMode()?.bearbeitung();
if (editing === undefined) {
field.hidden = true;
return;
}
const p = adapter.project(editing.ort); // a world coordinate; follow it when the view changes
field.style.left = `${p.px}px`;
field.style.top = `${p.py}px`;
if (field.hidden) {
field.hidden = false;
field.value = editing.text;
field.focus();
}
};
field.addEventListener('input', () => textMode()?.tippe(field.value));
field.addEventListener('keydown', (e) => {
e.stopPropagation(); // the letters belong to the field, not to the toolbar shortcuts
if (e.key === 'Enter') textMode()?.abschliessen(); // writes, in one transaction
if (e.key === 'Escape') textMode()?.verwerfen(); // discards
update();
});
// A click on the map opens the input without changing data, so listen to the pointer as well.
adapter.on('pointerup', update);
adapter.on('viewchange', update);
for (const event of ['modechange', 'change', 'unchanged', 'rejected'] as const) editor.on(event, update);
return field;
}
mode.bearbeitung()returns the running input —ort(world coordinate),text,groesse,farbe,fett, andvorhanden(whether the element already exists) — orundefined.mode.tippe(text)takes the typed text; nothing is written yet.mode.abschliessen()writes it in one transaction (set-label);mode.verwerfen()discards. Clicking elsewhere on the map also completes the running input.- An empty text on an existing label removes the label and its style; an empty text on a new spot writes nothing.
The test of this example labels an empty spot (a new point with text: 'Well'), opens a field on a
parcel and discards it with Escape, then labels the parcel with Enter.
API: attributeVon,
neuerAttributsatz,
Editor.benenneElement,
Editor.gestalteText,
TextMode.