{% import "partials/icons.html" as ic %} {# Metadata-driven form controls. Every control is chosen from a FieldSpec's `kind`, never from the primitive's type, so registering a new action / condition / constraint in Python gives it a working inspector with no change to this file. #} {% macro control(spec, value, document, prefix='', draft=none, params={}) %} {%- set name = prefix ~ spec.name -%} {%- set current = spec.default if value is none else value -%} {% if spec.kind == 'bool' %} {% elif spec.kind == 'select' %} {% elif spec.kind == 'entity' %} {% elif spec.kind in ('lanelet', 'lanelet_list') %} {# The readout only. Its map lives in a modal that `partials/lanelet_search.html` renders outside this form -- the choice between a pinned lanelet and a constraint search is made of forms, and a form cannot nest. Anything rendering `fields()` renders `modals()` too. #} {{ lanelet_field(name, current, draft, many=(spec.kind == 'lanelet_list')) }} {% elif spec.kind == 'action' %} {# Actions are listed by track, because "which one is Cut in" is a question about whose track it sits on as much as about its title. #} {% elif spec.kind == 'signal_controller' %} {# The document already says which junctions it runs. Offering them beats a text box whose only feedback is a validation error after the fact. #} {%- set declared = document.map.traffic_signal_controllers -%} {%- set names = [] -%} {%- for controller in declared %}{% set _ = names.append(controller.name) %}{% endfor -%} {% elif spec.kind == 'signal_phase' %} {# Scoped to the controller the card names, because a phase only means anything inside one. The form re-renders on change, so choosing a controller refills this list. #} {%- set chosen = params.get('controller') -%} {%- set declared = document.map.traffic_signal_controllers -%} {%- set scoped = declared|selectattr('name', 'equalto', chosen)|list if chosen else declared -%} {%- set names = [] -%} {%- for controller in scoped %}{% for phase in controller.phases %}{% set _ = names.append(phase.name) %}{% endfor %}{% endfor -%} {% elif spec.kind in ('int_list', 'int_list_or_ref') %} {% elif spec.kind == 'int' %} {% elif spec.kind == 'number' %} {% else %} {% endif %} {% endmacro %} {# The readout and the button that opens the map. Split from the modal so a caller can put the two in different places: the modal holds forms -- the spawn's constraint tree -- and a form cannot sit inside the inspector's own form. The halves find each other by id, not by being adjacent. #} {% macro lanelet_field(name, current, draft, many=false) %} {%- set shown = (current if current is string else (current or [])|join(', ')) if many else ('' if current is none else current) -%} {# The map is the only way to set a lanelet: the number is a readout, not an input. Nobody knows lanelet ids by heart, so typing one is guesswork that the map answers exactly -- and making the map the single editor means there is one path a value can arrive by, not two to keep in step. There is no "type it instead" fallback for a viewer that will not load, because the page fetches htmx from a CDN too: an editor that cannot reach the network has no working controls at all, so the fallback would only ever run when nothing else does. #}
{# A text input even for a single id: `type=number` would let a browser offer spinners on a field nothing may type into. #} {% if draft %} {% endif %}
{% endmacro %} {# One mounted Lanelet2 viewer, wherever it is drawn. The data attributes *are* the contract with `editor.js` -- which module to load, which .osm, what to outline, which layers a click may land on -- so they are written once here rather than at each site that shows a map. The picker and the places panel differed by two attributes and drifted by three the first time the contract grew a term. #} {% macro map_frame(key, draft, highlight, picks_into='', many=false) %} {% endmacro %} {# The map, over the whole viewport: the inspector column is 384px wide and a city map that size cannot be picked from. Opening it is also the first time the map is fetched and parsed, instead of on every inspector render. A caller block is rendered beside the map. That is where every lanelet field puts the choice between a pinned lanelet and a constraint search, and the search itself: a constraint is a statement about the map, and is easier to write with the map it searches in front of you. See `partials/lanelet_search.html`, which is what fills that block. #} {% macro lanelet_modal(name, current, draft, label='Lanelet', many=false) %} {%- set shown = (current if current is string else (current or [])|join(', ')) if many else ('' if current is none else current) -%} {% if draft %} {% endif %} {% endmacro %} {% macro field(spec, params, document, prefix='', draft=none) %} {%- set value = params.get(spec.name, spec.default) -%}
{% if spec.kind != 'bool' %} {% endif %} {{ control(spec, value, document, prefix, draft, params) }} {% if spec.help and spec.kind != 'bool' %}

{{ spec.help }}

{% endif %}
{% endmacro %} {% macro fields(specs, params, document, prefix='', draft=none) %} {% for spec in specs %}{{ field(spec, params, document, prefix, draft) }}{% endfor %} {% endmacro %} {# A condition rendered as `subject -> target | metric | value unit`, which is the reading the swimlane is built around. A target that is not an actor carries the name of what it is -- `Lanelet 183` rather than `183` -- because this framework speaks both Lanelet2 and OpenDRIVE and a bare number belongs to neither on sight. #} {% macro condition_summary(node, document) %} {%- set spec = get_condition_spec(node.type) -%} {% if spec %} {%- set visual = spec.visual -%} {%- set subject = node.params.get(visual.subject) if visual.subject else none -%} {%- set subject_action = document.action(subject|string) if subject else none -%} {%- set target = node.params.get(visual.target) if visual.target else none -%} {%- set value = node.params.get(visual.value) if visual.value else none -%} {%- set rule = node.params.get(visual.rule) if visual.rule else none -%} {#- "world" is for a condition that names nothing because it is about the world -- a collision, a timeout. A condition that *does* declare a reference but has not been given one is a different thing entirely, and showing it as "world" made an unconfigured card look finished. It says what it is waiting for instead. -#} {%- set declares_ref = visual.subject is not none or visual.target is not none -%} {%- set ref_field = (spec.fields | selectattr('name', 'equalto', visual.subject) | first | default(none, true)) if visual.subject else none -%}
{% if subject_action %}{{ action_ref(subject_action, document) }} {% elif subject %}{{ entity_ref(subject, document) }}{% endif %} {% if subject and target is not none %}→{% endif %} {%- set target_choice = node.searches.get(visual.target) if visual.target else none -%} {% if target_choice and target_choice.deriving %} {{ visual.target_prefix }} {{ (target_choice.binding_title or 'derived')|lower }} {% elif target_choice and target_choice.searching %} {{ visual.target_prefix }} searched {% elif target is not none %}{{ entity_ref(target, document, visual.target_prefix) }}{% endif %} {% if not subject and target is none %} {% if declares_ref %} pick {{ (ref_field.label if ref_field else 'a reference')|lower }} {% else %} world {% endif %} {% endif %}
{{ visual.metric }}
{#- Each detail is labelled by the same FieldSpec the inspector renders, so the card and the form cannot drift apart on what a value is called. A required one left unset is stated rather than omitted: "Road 80" alone looks like an address, and a road is not one until a lane is named. An optional bound that is simply absent says nothing, because "no upper limit" is the ordinary case rather than a gap. -#} {%- for name in visual.details %} {%- set field_spec = (spec.fields | selectattr('name', 'equalto', name) | first | default(none, true)) -%} {%- if field_spec %} {%- set shown = node.params.get(name) -%} {%- if shown is not none %}
{{ field_spec.label }} {{ shown }}{{ ' ' ~ field_spec.unit if field_spec.unit }}
{%- elif field_spec.required %}
any {{ field_spec.label|lower }}
{%- endif %} {%- endif %} {%- endfor %}
{% if rule %}{{ rule_symbol(rule) }}{% endif %} {% if value is not none %}{{ value }}{% if visual.unit %} {{ visual.unit }}{% endif %} {% elif visual.value_label %}{{ visual.value_label }}{% endif %}
{% else %}
Unknown condition “{{ node.type }}”
{% endif %} {% endmacro %} {% macro entity_label(value, document) %} {%- set entity = document.entity(value|string) -%} {%- if entity -%}{{ entity.display_name }}{%- else -%}{{ value }}{%- endif -%} {% endmacro %} {# An actor's colour comes from its position in the document order, so the same actor is the same hue on its track header, on its clips and on every condition that names it. That tint is the only thing pointing a reaction back at the track that causes it: an ego action triggered by "NPC1 is inside lanelet 183" hangs in the ego's own slot, not on a wire back to NPC1. #} {%- set ACTOR_HUES = ['var(--actor-1)', 'var(--actor-2)', 'var(--actor-3)', 'var(--actor-4)'] -%} {% macro entity_hue(value, document) %} {%- set entity = document.entity(value|string) -%} {%- if entity is none -%} {%- elif entity.kind == 'ego' -%}var(--actor-ego) {%- else -%} {%- set order = document.ordered_entities()|map(attribute='id')|list -%} {{- ACTOR_HUES[order.index(entity.id) % ACTOR_HUES|length] -}} {%- endif -%} {% endmacro %} {# An action named by a condition reads as " on <track>": the title alone is ambiguous once two actors both have a "Lane Change". #} {% macro action_label(action, document) %} {%- set spec = get_action_spec(action.type) -%} {%- set title = action.title or (spec.title if spec else action.type) -%} {%- set owner = document.entity(action.actor|string) if action.actor else none -%} {{- title }} · {{ owner.display_name if owner else 'World' -}} {% endmacro %} {# The action a condition watches wears the track colour of the actor that runs it, so the card says which track to look at even before the link is traced. #} {% macro action_ref(action, document) %} {%- set hue = entity_hue(action.actor, document)|trim if action.actor else '' -%} {%- if hue -%} <span class="cond-actor" style="--ref-hue: {{ hue }}">{{ action_label(action, document) }}</span> {%- else -%} <span>{{ action_label(action, document) }}</span> {%- endif -%} {% endmacro %} {# A subject or target that names an actor is tinted; anything else is left untinted, because only actors have a track to point back at -- but it is prefixed with what it is, so a lanelet id is never mistaken for an OpenDRIVE one. #} {% macro entity_ref(value, document, prefix='') %} {%- set hue = entity_hue(value, document)|trim -%} {%- if hue -%} <span class="cond-actor" style="--ref-hue: {{ hue }}">{{ entity_label(value, document) }}</span> {%- elif prefix -%} <span class="cond-ref"><span class="cond-frame">{{ prefix }}</span> {{ entity_label(value, document) }}</span> {%- else -%} <span>{{ entity_label(value, document) }}</span> {%- endif -%} {% endmacro %} {# The choice of binding and its fields: how a derived value -- a spawn's offset, or a lanelet -- is worked out from the lanelet a search picks. `produces` narrows the choice to the bindings that work one out. #} {% macro binding_picker(produces, binding, default_type, label, document, draft) %} {%- set binding_type = binding.type if binding else default_type -%} {%- set spec = get_binding_spec(binding_type) -%} <div class="ed-field"> <label class="ed-label">{{ label }}</label> <select name="binding_type" class="ed-input"> {% for option in binding_specs(produces) %} <option value="{{ option.type_id }}" {% if option.type_id == binding_type %}selected{% endif %}> {{ option.title }} </option> {% endfor %} </select> {% if spec and spec.description %} <p class="ed-help">{{ spec.description }}</p> {% endif %} </div> {% if spec %} {{ fields(spec.fields, binding.params if binding else {}, document, 'binding_', draft) }} {% endif %} {% endmacro %}