Documentation

Pro handbook

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 text and 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?) sets text on all given elements in one transaction (rename), trimmed. An empty text removes the whole label — text and 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 as undefined removes 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 (an EbenenId) 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, and vorhanden (whether the element already exists) — or undefined.
  • 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.

See it on a map.