Pro documentation

Free handbook

Rules between layers (Pro)

The free editor checks the two kinds 'innerhalb' and 'ueberlappungsfrei' (see Rules between layers). @vectorstream/pro adds the following, through the extension interface; the storage format, the refusals and the counting of legacy are the free editor's and stay as described there. Install it with import '@vectorstream/pro/global' (every editor built afterwards) or new Editor(…, adapter, { erweiterungen: [proErweiterung()] }).

What Pro brings:

  • Ten more kinds of rule, below.
  • Attribute conditions on a rule, see Conditions.
  • Meters: distance, minimum area and minimum width measured with the metric of the map, see Meters.
  • Snapping onto the target layers of the active rules, see Snapping.
  • The red preview of the tools, see The red preview, checked incrementally.
  • The list of violations and the rules dialog of the application, see below.

An example with a condition, a kind without a target layer and the helper for rules that block both directions:

import type { Editor, Regelverstoss } from '@vectorstream/editor';
import { BUILDINGS, PARCELS } from '../../handbuch/beispiele/layers';

/**
 * Two rules on the buildings layer: every building lies within one parcel, and every building
 * labeled `use = house` has an area of at least 20 000 m². Adding a rule is never refused because
 * of data that is already there; what violates it is returned as the legacy.
 */
export function addBuildingRules(editor: Editor): { within: string; large: string; legacy: readonly Regelverstoss[] } {
  const within = editor.regelHinzufuegen(BUILDINGS, { art: 'innerhalb', ziel: PARCELS });
  const large = editor.regelHinzufuegen(BUILDINGS, {
    art: 'mindestflaeche',
    mass: 20_000, // square meters; this kind has no target layer
    bedingung: { schluessel: 'use', op: 'gleich', wert: 'house' },
  });
  if (within === undefined || large === undefined) throw new Error('a rule was rejected');
  return { within, large, legacy: editor.regelVerstoesse() };
}

/**
 * Runs `work` with a rule switched off and switches it on again afterwards, even if `work` throws.
 * The way to edit against a rule that blocks both directions, such as `'lueckenlos'`. Each of the
 * three steps is its own undo step.
 */
export function withRuleOff<T>(editor: Editor, ruleId: string, work: () => T): T {
  editor.regelAendern(ruleId, { aktiv: false });
  try {
    return work();
  } finally {
    editor.regelAendern(ruleId, { aktiv: true });
  }
}

Its test uses the sample parcels and the sample house, which lies across the boundary of two parcels: both rules report the house as legacy (it is in no single parcel, and its 11 000 m² are below 20 000), a condition use = shed switches the second rule off for it, and withRuleOff leaves the rule on afterwards, also when the work throws. The test gives the null adapter the metric of an earth map; see Meters.

Kinds of rule#

The terms are those of the free page: an element of the constrained layer is every bounded area, every line chain and every free point; every bounded area of the target layer is a target area; the boundary of the target layer is formed by the edges that border one of its areas.

Group Kind (dialog text) art An element violates when … Checked elements Measure
Position to areas must touch 'beruehrt' it has no contact with a target area, or overlaps one. areas, lines, points of X —
must not touch 'beruehrungsfrei' it touches or overlaps a target area. areas, lines, points of X —
must overlap 'schneidend' it overlaps no target area. areas, lines, points of X —
Coverage must cover 'abdeckung' an element of Y does not lie wholly under areas of X. areas, lines, points of Y —
must fill without gaps 'lueckenlos' the same, or an element of X reaches beyond the target layer. Y and X —
Lines and points must lie on a line of 'aufLinie' a line or point is not (everywhere) on a line of Y. lines, points of X —
must lie on the boundary of 'aufRand' a line or point is not (everywhere) on the boundary of Y. lines, points of X —
Distance must keep a distance from 'mindestabstand' it comes closer than mass meters to an area (with its interior), line or point of Y. areas, lines, points of X m
Shape minimum area 'mindestflaeche' an area is smaller than mass square meters. No target layer. areas of X m²
minimum width 'mindestbreite' no circle of diameter mass meters fits into an area. No target layer. areas of X m

The two original kinds, 'innerhalb' and 'ueberlappungsfrei', are free. A rule of one of the ten kinds below is kept unchanged and not checked by an editor without Pro (see Compatibility).

Notes on single kinds:

  • Touching follows DE-9IM. An element has contact when a part of its boundary (a point: the point itself) is at most one tolerance away from the boundary of a target area; it overlaps when it reaches into the interior of one. 'beruehrt' needs contact without overlap, 'beruehrungsfrei' forbids both, 'schneidend' needs overlap. A line that runs from outside into an area therefore crosses it and does not merely touch it.
  • "Line in area" (a pipe within its corridor) is not a kind of its own: it is 'innerhalb' applied to a line.
  • 'aufLinie' and 'aufRand' check lines and points only; an area of X is never checked by them. A line chain must lie on the target everywhere: it may follow several target edges one after the other. The tolerance is the one in Touching and tolerance.
  • 'abdeckung' checks the elements of the target layer. A violation still belongs to the rule's layer X, but its element belongs to Y: Regelverstoss.elementEbene names that layer. Always resolve a violating element against elementEbene, never against ebene. For all other kinds the two are equal, except for the X part of 'lueckenlos'.
  • 'lueckenlos' means "X fills Y": every element of Y is covered, and nothing of X lies outside the target layer. The classic case is "parcels fill the municipality".
  • 'mindestabstand': an element in, over or on a target area has distance zero and violates. This includes an element in a hole of a target area: the hole is an area of its own, so the distance to it is zero too. Areas of X are measured to the target from their boundaries and from the inside.
  • Minimum area and width judge every area for itself, an enclosed courtyard separately. The area is the area of the outline minus its holes.

Gaps are areas#

The editor's topology turns every enclosed region into an area. A gap between parcels that is enclosed on all sides is therefore itself an area of the parcel layer, perhaps without a label, and it covers: 'abdeckung' and 'lueckenlos' cannot see it. The same holds for a courtyard inside a building layer. To treat unlabeled areas as gaps, give the rule a condition that only the real areas satisfy, for example "nr is filled". Only a gap that is open to the outside (the unbounded face) is found without a condition.

Working with 'lueckenlos'#

While a 'lueckenlos' rule is active and satisfied, counting blocks drawing in both layers. Take "buildings fill parcels": a new parcel is refused because no building covers it yet (a gap), and a new building outside the parcels is refused because it reaches beyond the target layer. Neither can be drawn first.

The way out: turn the rule off, draw, turn it on again (withRuleOff above). Switching a rule on is never refused because of the data, so what is still wrong afterwards is merely legacy and listed in regelVerstoesse(). Each of the three steps is its own undo step. The editor API has no call that combines a drawing and a rule change in one transaction.

Conditions#

A rule can carry one condition on one attribute: { schluessel, op, wert? } with op one of

op Dialog text Satisfied by an element …
'gleich' = whose attribute is set and has the text wert.
'ungleich' ≠ without the attribute, with null, or with another text than wert.
'belegt' is filled whose attribute is set and not the empty text.
'unbelegt' is empty without the attribute, with null, or with the empty text.
  • Values are compared as text: the number 5 equals '5', and true equals 'true'. null and a missing attribute both count as not set. There are no numeric comparisons, no "and"/"or", and no second condition.
  • wert belongs to 'gleich' and 'ungleich' and must not be empty there; 'belegt' and 'unbelegt' take none. An incomplete condition, an empty key and any key starting with vs: (reserved for internal data) are refused with ruleConditionInvalid.
  • A condition selects elements of the constrained layer: only those are checked. Areas read their attributes, line chains the attributes of their line, points those of their node. For 'abdeckung' and 'lueckenlos' the condition selects the covering areas of X: an area that does not satisfy it counts as a gap, which is how unlabeled areas become gaps (see above). It never selects elements of Y.
  • Because a condition looks at attributes, an edit of attributes can be refused: if giving a violating building the label or attribute that the condition asks for raises the number of violating elements, the edit is rejected like a drawing would be. That is new for an edit that moves no geometry.

Meters#

'mindestabstand', 'mindestflaeche' and 'mindestbreite' measure in meters. The meaning of a meter comes from the metric of the map (Adapter.metric()), the same computation as the measurement display: on Leaflet, OpenLayers and MapLibre maps a sphere with radius 6 371 000 m, not the WGS84 ellipsoid; the two differ by up to about 0.5 %. On a map without an earth (the null adapter, L.CRS.Simple) a "meter" is one coordinate unit, so the same document can be judged differently on such a map. The sample data of this handbook are degrees; a test that uses them with the null adapter must give it the earth metric (kugelMetrik() from @vectorstream/adapter), as the test of the example above does.

Tolerance in meters. τₘ is τ times the larger scale at the place, about 1.66 mm with the default grid. Everything is decided in the user's favor: a distance of at least mass − τₘ is kept; an area is large enough when area + τₘ · perimeter reaches mass; a width is wide enough when a circle of radius mass/2 − τₘ fits.

Accuracy of the distance. The distance is computed in a flat frame around the place and then measured exactly with the metric between the two closest points found, so the value is a real distance between points of both elements and never too small. It can be a little too large. Up to 1 km the excess stays below τₘ (≈ 1.66 mm). Beyond that it grows with the distance and the latitude. Measured excess of the computed distance over the true minimum:

Latitude 100 m 1 km 10 km 100 km
51.5° 0 0.005 mm 4.8 mm 4.8 m
60° 0 0.008 mm 5.5 mm 9.0 m
75° 0 0.04 mm 43 mm 34 m
85° 0.0004 mm 0.34 mm 380 mm 325 m

At 10 km that is some millimeters up to a few decimeters depending on the latitude, at 100 km it is in the range of meters. The excess is never negative, so the error only goes one way: a rule can miss a violation whose true distance lies in the band from mass − τₘ − excess to mass − τₘ, but it never reports a violation that is not one.

Poles and the antimeridian. The checks are planar: they never wrap around and they look for neighbours in a box of degrees derived from the local meters per degree.

  • Antimeridian. Elements on either side of ±180° longitude count as far apart, for every kind of rule. A pair that is close across the line is therefore reported as keeping its distance ('mindestabstand', 'beruehrungsfrei': a false "no violation" that can hide a real one), and a rule that demands contact ('beruehrt') reports a violation although the areas touch. Data that continue past 180° (longitudes above 180 or below −180, not folded back) are checked like any other data.
  • Poles. Close to a pole a degree of longitude is only a few decimeters, and the meters lose accuracy. For 'mindestabstand' the search looks at all longitudes as soon as the reach spans so many degrees of longitude that the local plane frame is no lower bound any more, so a pair at a large longitude difference is found when it is nearer than mass over the pole (two points 11 m from the pole, 170° apart, 22 m: a rule of 25 m is violated). Remaining limit: the distance that is measured is taken between the closest points of the plane frame, so close to the pole it is an upper bound of the true distance, not the exact minimum; a rule can miss a pair whose true distance lies only slightly below mass.

The incremental check gives the same answer as the full one in all of these places (random edit sequences across ±180°, next to and on both poles, and with 7 km units are compared in the test suite); the limits above are limits of the full check.

Minimum width#

"Minimum width" means: a circle of that diameter fits somewhere into the area. It finds slivers that are narrower than mass everywhere. It does not find a narrow neck in an otherwise wide area: a dumbbell with a thin bar between two broad heads has room for the circle in a head and passes.

The search for the largest inscribed circle examines at most 20 000 cells per area. A long thin area keeps about L/w cells alive and runs into that limit before the search can decide. When the limit is reached, the check asks one more question that needs no cells: is the convex hull of the outer ring narrower than the circle, in some direction? If so, no circle of that diameter can fit, and the area is reported, however long it is. A long convex sliver (a strip, a triangle, a thin quadrilateral; also with many vertices) is therefore found at any length. Measured: a strip of 10 km by 1.7 m against 2 m, or 1000 by 0.9 against 1, is caught; before this addition a strip passed from about L/w = 4000 on when the required width was just above its width.

What is still missed is a long sliver that is not convex: a bent or V-shaped band whose hull is wide, or a ring that is thin everywhere and very long (an annulus). Their cell search runs out of the 20 000 cells and lets the area pass, in the user's favor (a false "no violation"). The hull proof itself never reports an area into which the circle fits, measured in the flat frame of the area (see Accuracy of the distance above): on the sphere the frame has a small relative error, so a very long strip can be reported although its true width at one end is a little above the measure (a north-south strip of 100 km by 1.99 m at 51.5° against 2 m is reported, its true width at the south end being 2.0096 m; at 10 km the difference is 0.1 %). The cell search has the same frame. Measured with L the length and w the width: such an area passes when L/w ≳ 20 000 in general, or when the required width exceeds w by less than about L/24 000. A finer start grid does not close this gap: it is the number of cells, not the start grid, that runs out (measured with start grids up to 64 times finer: the same areas are missed, and raising the limit tenfold costs about ten times as much for the areas that reach it). For such shapes use 'mindestflaeche', or split the long area.

Messages below the display's rounding#

The text of a violation names the measure as the measurement display writes it. The display rounds: areas under 10 000 m² to whole square meters, lengths under 1 km to 0.1 m. A rule 'mindestflaeche' with mass: 0.4 therefore says "smaller than 0 m²", which is misleading but follows the display. Choose measures that the display can show.

Snapping#

A layer with an active rule also snaps, while drawing and editing, onto the layers its rules name as targets, after its own geometry (a real element of its own layer always wins). Two kinds are left out because snapping onto the target would always violate: 'beruehrungsfrei' and 'mindestabstand'. Kinds without a target do not snap. No topology arises between the layers: moving a parcel boundary later does not move the building. See Snapping. Without Pro the editor snaps onto the active layer only, also when an active rule names a target.

The red preview#

While you draw or move, the preview of a tool turns red when the next action would be refused: the ghost while moving, the rubber band while drawing, the outline of a rectangle, a circle or a stamp, the line of a cut. These tools show it: move, edit (dragging a vertex), polygon, line, point, rectangle, circle, stamp, freehand, cut, paste and copy, and straighten. Red announces a refusal by any validator (geometry or a rule), not only by a rule.

  • What red means. Releasing or clicking right there would be refused. In the tools that collect points (polygon, line, cut) the preview is checked as if the pointer position were the next point, because that is what a click adds, starting from the rubber band. Enter without a further click finishes without that point and can differ from what the preview showed.
  • Only with an active rule. Red appears only while an active rule concerns the active layer, that is, the layer owns the rule or is its target layer. Without one the preview stays as it always was, and a refusal for geometry alone (for example a ring that crosses another area) is not shown in red either; the edit is refused on release as before.
  • Mistakes in the input are never red. If the tool itself refuses the input, the preview looks as usual and the refusal comes on release: a stamp that is too dense (tooDense), a detached copy that would cross the target boundary (detachTargetCrossesBoundary), a copy with holes (copyHoles), an outline too close to the edge (outlineNearEdge) and an insertion that is too narrow (insertTooNarrow).
  • Delay. The check is throttled, at most about once every 100 ms, and on large layers it runs between pointer events. The colour can therefore lag the pointer by a moment, and it always shows the answer for the last position that was checked. The check made when you release or click is never throttled and always decides. Straighten is not throttled.
  • Legacy in the scene. Elements that already violate a rule (the legacy, see below) are drawn in the invalid state in the scene as long as they violate. A selected or hovered element keeps its own look.

Performance of the preview#

The check of the preview does the work of a trial edit, and since the check is incremental it does it only where the edit can change something: the geometry check, the spatial indexes and every rule re-examine only the elements near the change, and a rule keeps the verdict of every other element. The result is always the one the full check would give (a test suite compares them on every call). Measured on an Apple M5 Pro, with a rule 'innerhalb' on both layers and 40 pointer moves 16 ms apart:

Areas per layer Trial edit plus redraw Gap between two pointer moves (largest)
100 about 2 ms about 18 ms
400 about 5 ms about 20 - 35 ms
1024 about 12 ms about 30 - 40 ms
2025 about 25 ms about 35 - 60 ms, the very first one about 90 ms

The first trial of a gesture is the slowest: it builds what the following ones reuse (about 90 ms at 2025 areas). Without an active rule there is no trial and no timer. The trial edit runs at most every 100 ms and not at all in the pointer path unless the layer is fast (under 8 ms), so a large layer shows red a little late, never blocks a pointer event. Not incremental, and therefore as slow as before: creating or changing a rule, opening a document and a change that touches most of a layer (a full check of the rule is run then, mindestabstand and mindestbreite with the speed-ups of the first stage). A large target area (a municipality) makes every edit inside it re-examine every area it covers. The incremental check keeps its bookkeeping per layer (about 7 MB more memory at 2025 areas per layer); it also makes editing large layers faster when no rule is set.

Changing the kind#

A kind change drops a ziel or mass that the new kind cannot take. A kind change between 'mindestflaeche' (m²) and 'mindestbreite' (m) keeps an existing mass without any conversion unless the change brings a new one, so pass mass along.

Codes of the rejections#

A rejection by one of the ten kinds has the rule 'geofencing' and one of these codes: editor.ruleViolationTouch, …NoTouch, …Intersect, …Cover, …Fill, …OnLine, …OnBoundary, …Distance, …MinArea, …MinWidth. The codes keep the prefix editor. they have always had, although the sentences are in the catalog of Pro (namespace pro, registered with registerLocale('de', proMessages, proDe)). The params are those of the free kinds.

The application#

The application in app/ offers all kinds in a rules dialog of the layer: the kind as a list with the groups of the table above, the target (a dash for a kind without one), the measure with its unit and a "+ Condition" row with key, comparison and value. Unlike the library, the dialog proposes values: changing the kind to one with a measure sets 5 m for the distance, 2 m for the width and 10 m² for the area, because a number from the previous kind would mean something else (the library takes it over unconverted, see above). "+ Add rule" picks the first combination of kind and target that does not exist yet. A condition that is still incomplete stays a draft in the dialog and is marked invalid.

The list of violations#

Above the layer tree the application lists the violations of all active rules, one entry per violating element (a line chain counts once). The box sits above the tree so that it is in view however long the tree is (as long as the tool row and the rules dialog fit into the layers panel; with many open rules or a very low window the panel itself scrolls); the tree takes the remaining height and scrolls in itself (never below about 120 px), and the list is capped at about a quarter of the window height and scrolls in itself too, so the heading with the count is always visible. With violations the box has a red bar at its left edge; with rules but without violations it shrinks to one quiet line. The heading counts them and is announced to screen readers when the number changes; with rules but without violations it says so, and without any rule the list is hidden. An entry names the layer, the kind of element and its label, and in a second line what is wrong, worded for each of the twelve kinds. At most 100 entries are drawn, the rest is summed up in one line.

A click, or Enter or Space on the focused entry, moves the map to the element with zeigeElement, activates the layer the element belongs to (for 'abdeckung' and 'lueckenlos' that can be the target layer) and selects the element. A locked layer is shown but cannot be activated: the app then shows the editor's message as a toast and selects nothing. The list keeps its buttons while it updates and gives the focus back to the focused entry if it had to move it; when that violation disappears, the focus moves to the entry that took its place. While the mouse button is held down on the list, an update waits until the click has been delivered, so a click is not lost when entries shift. A hidden layer is shown on the map, but not activated and nothing in it is selected; a toast says so. Both lines of an entry wrap, the list scrolls in itself, and the numbers use the language's format (2.500,5 m in German).

Performance#

The numbers below are those of the full check of a rule, the one that runs when a rule is created or changed, when a document is opened and when an edit touches most of a layer; after an ordinary edit Pro checks incrementally (see Performance of the preview). The budget "at most as much as checkGeometry of the layer" is met by most kinds but not by all. The numbers below are the median of 3 to 7 runs on an Apple M5 Pro (Node 26); measurements scatter by up to about ±45 % between runs (a second run differed by more than 30 % in several cells; ranges are given where it was repeated); the ratio is the time of the rule check divided by checkGeometry of the same layer, with 2025 areas per layer, rotated squares, an index in place.

Kind Buildings each in one parcel Buildings across boundaries
'innerhalb', 'ueberlappungsfrei', 'beruehrt', 'beruehrungsfrei', 'schneidend' 0.59 – 0.62 0.64 – 0.66
'abdeckung' 1.01 1.1 – 1.4
'lueckenlos' 1.36 0.9 – 1.6
'mindestabstand' (0.1 m), 'mindestflaeche', 'mindestbreite' (compact shapes) 0.02 – 0.05 0.02 – 0.05
  • 'abdeckung' exceeds the budget at 2025 areas (1.0 to 1.4 in both runs); 'lueckenlos' is a borderline case: between 0.9 and 1.6 depending on the run. With fewer areas the ratio is higher: for buildings across boundaries 4.6 ('abdeckung') and 5.6 ('lueckenlos') at 100 areas (400 areas: 2.2 – 3.5 and 2.2 – 4.1), because fixed costs dominate when checkGeometry itself takes about 1 ms.
  • Identical cells in X and Y (every building ring equals a parcel ring) are expensive for all kinds with a target: 'innerhalb' 12.5×, 'ueberlappungsfrei', 'beruehrt', 'beruehrungsfrei' and 'schneidend' 12.8 – 12.9×, 'lueckenlos' 8.2×, 'abdeckung' 4.4× at 2025 areas.
  • 'aufLinie' and 'aufRand' with 2025 lines or points stay at 0.01 – 0.06.
  • 'mindestbreite' on long thin areas is the most expensive form check, but cheap enough: the distance of a probe point to the boundary goes through an edge index per area, so the cost no longer grows with cells times edges. The search still stops at the limit of 20 000 cells. A bar of 1000 × 1 against a width of 1.01 takes 50 ms with 1000 vertices and 100 – 106 ms with 10 000 (formerly 1.7 s and 16.7 – 19.6 s); ten such bars with 1000 vertices each take 0.5 s (formerly 17 – 19 s). A strip of 200 × 0.9 takes 4 ms (1000 vertices) or 18 ms (10 000 vertices), a ring 0.9 wide with 10 000 vertices 43 – 53 ms (formerly 3.9 – 7.5 s). For round areas (a circle of radius 100, 10 000 × 10 circles) the ratio is at most 0.26. The results are identical to those of the former search.
  • 'mindestabstand' with a large measure and many elements no longer retrieves all target edges of the search box for every element: it looks at boxes growing in steps of two and stops at the first edge closer than the measure. Test case: 2025 areas against 2025 areas, all violating, the nearest target edge close to the element: ratio 0.1 – 0.2 at every measure from 100 m to 100 km on the sphere and up to 10 km on a plane (formerly 7.5 at 1 km, 118 at 10 km, 117 – 220 at 100 km); with 900 areas 0.2 – 0.3. Without a violation it stays at 0.06 – 0.15. Known limit: if the nearest target edge lies far from the element compared with the measure (more than a few percent of it), the search has to look at all target edges within that distance. Sphere, 2025 areas against 2025 areas, X starting 2 km east of Y, measure 10 km: 1.1 – 1.2 s, about 18 times checkGeometry (formerly 4.6 s, 93 times); with 900 areas 0.24 s (formerly 1.1 s). The results are identical to those of the former search.

If a rule makes editing feel slow, switch the rule off (aktiv: false), narrow it with a condition, or use a smaller measure.

Not included#

  • Narrow necks inside wide areas (a minimum width by morphological opening); see Minimum width.
  • Geodesy on the ellipsoid; meters follow the sphere of the measurement display.
  • More than one condition, comparisons of numbers or patterns, conditions on elements of the target.
  • Relations of other kinds against lines and points of the target than those in the table.

See it on a map.