HBW 2D-plan feature classes → lane XML (schema 1.1)
Summary
The customer (HBW) draws its 2D plans with a Grasshopper definition that reads our lane XML — the poster's yellow boxes are Grasshopper panels ({6;70} = data-tree paths). Per feature class it needs a short, ordered input list and joins the layer name against its own Data catalog (SYMB#### block, rotation mode DRW/OWN/NO/VEC/ORD). The catalog is customer-side; our XML only has to carry the input list.
All 18 poster columns collapse into three shapes — point symbol [layer, XYZ, direction, has-annotation, text], polyline [layer, has-description, sorting code, vertices], spline [layer, has-description, control points] — plus derived things we never export (hatches, tree-group symbol, guard-rail labels beyond a type attribute).
Proposal: schema 1.1, purely additive. Six optional elements appended after the last existing child of their parent (SchemaVersion, Layer, Direction, Annotation, Attributes, SortingCode, plus Polyline/Type). Feature/Type stays the semantic class id. ≈ 5 dev-days in this repo, none blocked on the customer; downstream producer PRs afterwards.
Sources: 2D plans overview.pdf (poster, source of truth), 240511_HBW - 2D plans.xlsx (priorities/status), 241213 HBW Presentation.pdf (KM-sign question). Markdown twin: docs/hbw_feature_classes_xml_plan.md.
Key decisions
- Purely additive, versioned. No existing element renamed, removed, reordered or made conditional. New elements are optional and appended after the last 1.0 child of their parent — safe for both name-based and index-based readers. Existing producers (cluster_stepper, guardrails) produce byte-identical output apart from
MetaData/SchemaVersion. Feature/Typestays the stable class id; the customer layer gets its own<Layer>.guardrails/lane_xml.pyand the customer definition filter on existingTypevalues (Axis of the Edge,Central Axis,Cross Section,guardrail…). Consumers resolve the catalog key asLayerif present, elseType(legacy DWG→XML files stored the layer inType).- One Feature = one customer layer = one container. Pedestrian roads may be polyline or spline — separate features, never the same path twice.
- Machine values in
<Attributes>, display strings in<Annotation>. The writer never formats numbers (decimal comma,Ø=, line breaks); producers pass display-ready text and the raw numbers ride along as attributes. - Uniform
<Attribute name= type=>form, sorted by name — generalises the block the guardrails stand-in exporter already invented (and whichcreate_xmlcurrently drops); its TitleCase-tag form is retired with a key adapter. - Sorting code is the pairing contract for trench/slope top+bottom (
G2$A/G2$B,B1$A/B1$B); the writer validates grammar and completeness at document level. Hatches are derived by the customer, never exported. - Writer ignores unknown dict keys (already true) and validates only what is load-bearing: sorting codes, annotation invariant, finite non-zero direction.
- Tree-group symbol (
Nutzungsart) not exported — poster says "no input geometry"; customer derives it from the tree-line blobs.
Schema 1.1 — new elements
| Parent | New child | Card. | Position | Purpose |
|---|---|---|---|---|
MetaData | SchemaVersion = 1.1 | 1 (always) | after PipelineVersions | readers branch on it; absence = 1.0 |
Feature | Layer | 0..1 | after NumberOfSplines | customer catalog key verbatim (PLC_Laubbaum, umlauts included) |
Feature | Attributes | 0..1 | after Layer | feature-level metadata (rare) |
Point | Direction (X,Y,Z) | 0..1 | after RoadPoint | symbol direction, unit vector in export frame, producer-normalised |
Point, Polyline, Spline, Line | Annotation (Display, Text*) | 0..1 | after last 1.0 child | poster "has annotation display" / "has a description" + text; one <Text> per line, no embedded newlines |
Point, Polyline, Spline, Line | Attributes (Attribute name= type=*) | 0..1 | last | extensible metadata |
Polyline | Type | 0..1 | after LaneID (same slot as Line/Spline since 0.8.0) | top/bottom, guardrail sub-kind |
Polyline | SortingCode | 0..1 | after Vertices | pairing token G2$B, B1$A |
Become optional for new producers only: RoadPoint on Point/Polyline/Spline; Spline/Fit_Points may be empty (still emitted). Legacy point fields always emitted; defaults when absent: PointerNumber = 1-based index, Color empty, FeatureClass = Feature/Type, FeatureID = Feature/ID. LaneID always emitted; non-lane features pass "0" (guardrails precedent).
Annotation invariants: Display required; Text required & non-empty when true, omitted when false. HBW producers always pass an annotation (even false) so the poster boolean is explicit; the writer emits it iff the key is present → legacy output unchanged.
Attributes: type ∈ string|integer|float|boolean, lower-case booleans, None skipped, nested values rejected, units in the name (height_m). Writer also accepts the guardrails polyline top-level keys (side, guardrail_type, faces, length_m, feature_type, width_m, top_height_p90_m, parent_guardrail_id, post_spacing_m, post_count, post_confidence, halfwidth_m) as attributes; explicit attributes wins.
Example — point symbol (Tree)
<Feature>
<Type>Tree</Type><ID>TREE-12</ID><LaneID>0</LaneID>
<Lines/><NumberOfLines>0</NumberOfLines>
<Points>
<Point>
<ID>TREE-12-1</ID><LaneID>0</LaneID><PointerNumber>1</PointerNumber>
<X>-109.06</X><Y>-383.29</Y><Z>0.0</Z>
<Color/><FeatureClass>Tree</FeatureClass><FeatureID>TREE-12</FeatureID>
<Direction><X>0.974467</X><Y>-0.224529</Y><Z>0</Z></Direction>
<Annotation><Display>true</Display><Text>Ø= 0,30/7,0m</Text></Annotation>
<Attributes>
<Attribute name="crown_diameter_m" type="float">7.0</Attribute>
<Attribute name="trunk_diameter_m" type="float">0.3</Attribute>
</Attributes>
</Point>
</Points><NumberOfPoints>1</NumberOfPoints>
<Polylines/><NumberOfPolylines>0</NumberOfPolylines>
<Splines/><NumberOfSplines>0</NumberOfSplines>
<Layer>PLC_Laubbaum</Layer>
</Feature>
Poster mapping: 0→Layer, 1–3→X,Y,Z, 4→Direction, 5→Annotation/Display, 6→Annotation/Text.
Example — paired polylines with sorting code (Slope)
<Polyline>
<ID>SLP-3-1</ID><LaneID>0</LaneID><Type>top</Type>
<Vertices><Vertex><ID>SLP-3-1-1</ID><X>…</X><Y>…</Y><Z>…</Z></Vertex>…</Vertices>
<SortingCode>B3$A</SortingCode>
<Annotation><Display>false</Display></Annotation>
</Polyline>
Grammar ^[A-Z]+[0-9]+\$[AB]$; A = top, B = bottom; index unique per family per document. Writer accepts a ready string sorting_code or the triple (sort_class, pair_index, pair_role) and composes it; rejects duplicates and incomplete pairs. Trench = two features (pPLC_Graben G<i>$A, pPLC_Grabensohle G<i>$B); slope = one feature, two polylines on pPLC_Böschung.
Example — spline (Bush) and annotation-only (guard rail)
<Spline>
<ID>HED-2-1</ID><LaneID>0</LaneID>
<Fit_Points/>
<Control_Points><Point><ID>HED-2-1_1</ID><X>…</X><Y>…</Y><Z>…</Z></Point>…</Control_Points>
<Annotation><Display>false</Display></Annotation>
</Spline>
<!-- guard rail: existing guardrails-package polyline, Type=guardrail -->
<Polyline>
<ID>segment_70_guardrail_00</ID><LaneID>0</LaneID>
<RoadPoint><From>0</From><To>24.8</To></RoadPoint>
<Vertices>…</Vertices>
<Annotation><Display>true</Display><Text>ESP 2,0</Text></Annotation>
<Attributes><Attribute name="guardrail_type" type="string">ESP 2.0</Attribute></Attributes>
</Polyline>
Cross-section points: Type stays Cross Section; add Layer (PLC_Punkte_Symbole, provisional), per-point Annotation (formatted Z) and Direction; PointerNumber/RoadPoint keep today's meaning.
Per-feature-class mapping
Type = English singular Title Case (house style of Cross Section); Layer = customer key verbatim. "Annot." = Annotation/Display.
| # | Poster column | Feature/Type | Layer | Container | Required | Annot. | Attributes / notes |
|---|---|---|---|---|---|---|---|
| 1a | Tree lines | Tree Line | sPLC_Laubbaumreihe | Splines | control points | false | crown symbols catalog-derived |
| 1b | Tree group symbols | — not exported | Nutzungsart | — | — | — | customer derives from tree-line blobs (Q10) |
| 2 | Bushes | Bush | sPLC_Hecke | Splines | control points | false | |
| 3 | Trees | Tree | PLC_Laubbaum | Points | position, direction | true — Ø= 0,30/7,0m | trunk_diameter_m, crown_diameter_m, later species_class |
| 4 | Fences | Fence | pPLC_Zaun | Polylines | ≥2 vertices | false | |
| 5 | Paving edges | Paving Edge | pPLC_Pflasterkante | Polylines | ≥2 vertices | false | |
| 6 | Noise blocking walls | noise_wall (existing) → Q7 | pPLC_Lärmschutzwand (Excel: polyline) vs sPLC_ (poster) | Polylines today | vertices | object name once agreed | height_m, length_m |
| 7 | Trenches (top) | Trench | pPLC_Graben | Polylines | vertices, G<i>$A | false | hatch derived |
| 7 | Trenches (bottom) | Trench Bottom | pPLC_Grabensohle | Polylines | vertices, G<i>$B | false | same <i> ties the couple |
| 8 | Slopes | Slope | pPLC_Böschung | Polylines ×2 | B<i>$A + B<i>$B, Type top/bottom | false | slope direction from A/B + geometry (Q6) |
| 9 | Pedestrian roads | Pedestrian Road | pPLC_Gehweg / sPLC_Gehweg | Polylines or Splines | vertices / control points | false | layer must match container |
| 10 | Terrain elevation points | Terrain Point | PLC_Geländepunkte | Points | position, direction | true — formatted Z | catalog mode NO ignores direction |
| 11 | Traffic signs | Traffic Sign | PLC_Verkehrszeichen | Points | position, direction | false | later height_m, plate_class |
| 12 | Shafts | Shaft | PLC_Schacht | Points | position, direction | false | |
| 13 | Cross section points | Cross Section (existing) | PLC_Punkte_Symbole (Q4) | Points | as today + direction | true — formatted Z | additive only |
| 14 | Emergency call boxes | Emergency Call Box | PLC_Notrufsäule | Points | position, direction | false | |
| 15 | KM signs | KM Sign | PLC_Kilometer-Tafeln | Points | position, direction | true — ≤3 lines (Q1) | km, section, station |
| 16 | Gullys | Gully | PLC_Gullys | Points | position, direction | false | width_m |
| 17 | Walls | Wall | pPLC_Mauerwerk | Polylines | ≥2 vertices | false | height_m, length_m |
| 18 | Guard rail annotations | guardrail (existing) | Schutzeinrichtung | Polylines | vertices | true — EDSP / ESP 2,0 | guardrail_type; no new geometry (Q8) |
| — | Hatches (7, 8) | — | — | — | — | — | never exported |
| — | Verge posts (Excel only) | Verge Post | PLC_Randpfosten (TBD) | Points | position, direction | false | slots into point shape |
| ✔ | Lane markings (exists) | Axis of the Edge, Center Lines | Q12 | Splines + Lines | as today | — | add Layer only |
| ✔ | Road axes (exists) | Central Axis … | — | Polylines | as today | — | unchanged |
| ✔ | Guard rails (exists) | guardrail, guardrail_support, guardrail_top_rail | Schutzeinrichtung | Polylines | as today | see 18 | attributes finally survive the Nexus exporter |
create_xml(data) contract
Dict keys per shape
feature: type (req), id (req, document-unique), lane_id ("0" if none), layer (req for HBW classes), attributes, lines|points|polylines|splines (absent ⇒ empty container + count 0).
point: id, position [x,y,z] req; lane_id, pointer_number, color, feature_class, feature_id, road_point optional (defaults above); direction [dx,dy,dz] req for HBW (finite, non-zero); annotation req for HBW; attributes.
polyline: id, vertices (≥2) req; lane_id, type, road_point [from,to], sorting_code or (sort_class, pair_index, pair_role), annotation (req for HBW), attributes; guardrails legacy keys accepted.
spline: id, control_points (≥2) req; fit_points (default []), lane_id, type, road_point, width, annotation, attributes.
line: unchanged + optional annotation, attributes.
annotation = {"display": bool, "text": str | list[str] | None}; attributes = Mapping[str, str|int|float|bool|None]; metadata.schema_version optional (writer defaults to "1.1").
Producer responsibilities: same geoshifted local frame/metres as today, unit direction vectors, display-ready text, globally unique ids.
Phases
Each phase = one PR in this repo. None needs a customer answer. ≈ 5 dev-days total.
1 · Compatibility freeze 0.5 d
- Golden fixture of today's full output: cluster_stepper-shaped dict (cross sections, lanes, axes) + guardrails-shaped dict.
- Snapshot test asserting exact element tree and child order.
2 · Writer plumbing + SchemaVersion 0.5 d
- Private helpers:
_text,_append_xyz,_append_annotation,_append_attributes,_append_direction,_sorting_code. - Emit
MetaData/SchemaVersionunconditionally; snapshot passes modulo that element.
3 · Feature + Point extensions 1 d
- Trailing
Layer/Attributeson Feature;Direction,Annotation,Attributeson Point; optional legacy point keys with defaults; optionalRoadPoint. - Tests: present/absent, ordering, Unicode layer/text, annotation invariant, zero-vector rejection, legacy-dict byte-identity.
4 · Polyline / Spline / Line extensions 1.5 d
Type,SortingCode(triple composition, grammar, document-level pairing check),Annotation,Attributes, optionalRoadPoint, emptyfit_points.- Guardrails legacy-key adapter → attributes.
- Tests: trench/slope pairing, malformed/duplicate/incomplete codes, guardrail parity with the stand-in exporter.
5 · feature_classes.py registry + builders 0.75 d
- Frozen map
class_id → (feature_type, default_layer, container)for every mapping row; builderspoint_feature(),polyline_feature(),spline_feature()return ready dicts so downstream repos stop hard-codingPLC_…. - Tests: registry covers the table; builders round-trip through
create_xml.
6 · Docs + sample fixture 0.75 d
docs/xml_schema.md: element grammar, 1.0→1.1 diff, one example per shape, consumerLayer→Typefallback; README link.tests/fixtures/sample_v1_1.xmlexercising every shape; one test dict per mapping row.
7 · Release 0.10.0 + customer acceptance 0.25 d
/wrap-up(bump, build, publish to Nexus, Jira-tagged commit).- Send sample 1.1 XML to HBW for a Grasshopper run;
lane_xml.pysmoke test.
Downstream follow-ups (other repos, after 0.10.0)
- guardrails: drop
_standin_create_xml, route throughcreate_xmlwithattributes/annotation0.5 d. - cluster_stepper: cross-section points gain
layer/direction/annotation; lane features gainlayer; behind a config flag until Q4/Q5/Q12 are answered 0.5 d. - New detectors (trees, signs, …) target the registry builders.
Risks
- high Index-based Grasshopper reader. If the customer's definition indexes
Featurechildren by position or rejects unknown children, even trailing elements break it. Mitigation: trailing placement + sample-file acceptance run before release (Q3); fallback = compat export mode. - med Poster vs Excel disagreements (noise wall spline vs polyline; tree-line "Annotation" vs Element; copy-paste errors in poster column texts). Mitigation: mapping table lists both; Q7 resolves.
- med Guard-rail label source. Legacy XML encoded it as
Point/FeatureClass=Guard Rails_EDSPon cross-section points; our pipeline emits guardrails as polylines. If the customer's definition only reads the legacy form, labels vanish (Q8). - med Detectors for most classes don't exist yet — the contract is designed ahead of producers; expect small dict-contract tweaks when the first detector lands. Registry builders (phase 5) contain the blast radius.
- low Deep tag search in
lane_xml.py(element.iter()by name forPoint/Vertex). New elements introduce no nestedPoint/Vertextags; X/Y are read as direct children — verified safe. - low Unicode layer names (
Böschung,Lärmschutzwand,Ø). Output is already UTF-8 viaprettify_xml; add an explicit test. - low Sorted attributes change nothing for existing output (no attributes emitted today) but must be documented so producers don't rely on insertion order.
Open questions
Answer each with number + default / alt 1 / alt 2. Defaults are what I'll build if you say nothing; most are internal decisions, a few must be confirmed by HBW.
- KM-sign annotation content (poster
[TBA]; drawing showsKM 46.5 / Abs. 90 / Station 0,25). What do we emit?default Three
<Text>lines in that exact order from attributeskm,section,station; lines whose value is unknown are omitted. Ask HBW whereAbs./Stationcome from; until then onlyKM …is populated.alt 1
Display=false+ numeric attributes only until HBW specifies the text; customer formats it in Grasshopper.alt 2 Single line
KM 46.5only; drop section/station entirely. - Tree annotation format —
Ø= 0,30/7,0min the sample,⌀=[trunk]/[crown]min poster text.default Emit the sample string verbatim (
Ø=, space, decimal comma, trunk 2 dp, crown 1 dp) and numeric attributestrunk_diameter_m/crown_diameter_m.alt 1 Numbers only (attributes),
Display=truewith no text — customer builds the string in Grasshopper.alt 2 Use the poster glyph
⌀and no space (⌀=0,30/7,0m). - Reader tolerance — does the Grasshopper definition tolerate unknown trailing children / is it name-based?
default Assume name-based and tolerant (true for
lane_xml.py; 0.8.0 already insertedTypemid-element without complaint). Ship 1.1 after one acceptance run on a sample file.alt 1 Add a writer flag
compat_schema="1.0"that suppresses every new element, so the same producer can serve both readers during transition.alt 2 Encode new data as XML attributes on existing elements (
<Point layer="…" dir="x y z">) — zero child-count change, but ugly for Grasshopper's XML nodes. - Cross-section layer name ("to be discussed as it is part of original XML").
default
Layer=PLC_Punkte_Symbole,TypestaysCross Section; confirm with HBW.alt 1 No
Layeron cross sections — customer keeps deriving the layer fromTypeas today.alt 2 Two layers:
PLC_Punkte_Symbolefor annotated points,PLC_Punktefor plain ones (Excel lists both;Punktemarked non-relevant Dec 2024). - Direction-vector semantics for
VEC/OWNrotation.default Unit vector in the XY plane, Z=0: road-axis tangent at the point for cross-section/terrain/KM/gully/shaft; plate-facing normal for traffic signs; always emitted even for
DRW/NOclasses.alt 1 Full 3D vector straight from the detector (sign-post axis, tree lean); customer projects to XY.
alt 2 Omit
DirectionforDRW/NOclasses (cross section, terrain, tree, call box); onlyVEC/OWNclasses carry it. - Slope hatch direction — is A/B + the two outlines enough?
default Yes; nothing extra. Top/bottom roles + 3D outlines determine the downhill side.
alt 1 Add attribute
downhill_direction(unit vector) on the top polyline.alt 2 Guarantee vertex correspondence (same count, same order) between the paired polylines so the customer can rule hatches 1:1.
- Noise walls — poster says spline
sPLC_, Excel says polyline-based, our detector emits polylines asType=noise_wall.default Keep polylines and the existing
noise_walltype string;Layer=pPLC_Lärmschutzwand; ask HBW to add a polyline catalog entry; object-name annotation off until specified.alt 1 Convert to spline (control points = vertices) on
sPLC_Lärmschutzwandto match the poster exactly.alt 2 Rename guardrails type strings to Title Case (
Noise Wall,Guard Rail) in a coordinated bump of both packages. - Guard-rail label source (
EDSP,ESP 2,0).default
guardrail_typeattribute +Annotation/Texton the guardrail polyline; customer places the repeated label along the rail.alt 1 Additionally emit guard-rail cross-section
PointswithFeatureClass=Guard Rails_<type>— the legacy shape the customer's definition may already read.alt 2
Display=falseuntil the detector reports the type reliably; attribute only. - Elevation label value/format (terrain + cross-section points).
default Absolute height (local Z + Geoshift Z), 3 decimals, dot separator (
47.333) — matches the sample drawing.alt 1 2 decimals with decimal comma (
47,33) for consistency with the tree label.alt 2 Geoshifted local Z; customer adds the shift in Grasshopper.
- Tree-group symbol (
Nutzungsart).default Not exported — poster says "no input geometry"; customer derives it from closed tree-line blobs.
alt 1 Export a closed outline polyline feature (
Type=Tree Group,Layer=Nutzungsart, add<Closed>true</Closed>).alt 2 Export one point feature at the blob centroid on layer
Nutzungsart. - Sorting-code index scope.
default Unique per family (
G/B) per XML document; unbounded integer (G10$Afine).alt 1 Unique per layer only (Graben and Grabensohle each restart at 1).
alt 2 Unique per road segment with a segment prefix (
G7-2$A) — needs HBW grammar change. - Layer names for the existing lane classes (
Axis of the Edgemixes solid splines + dashed lines).default No
Layeron lane/axis features in 0.10.0 — customer already maps them fromType.alt 1 Feature-level
Layerfrom the dominant segment type (Fahrbahnmarkierung_durchgezogen/_gestrichelt).alt 2 Per-segment
<Layer>on eachLine/Spline, derived from itsType.
Poster inconsistencies noticed: column texts copy-pasted wrong (5 & 9 say "Pavement", 6 says "Bushes", 13 says "Shafts"); tree-line catalog numbered 1–4 vs 0–3 in the screenshot; Gullys has Display=False yet a leftover Text km placeholder; terrain/cross-section examples show literal Text points.