A layer’s descriptionHtml is free HTML with one addition: text in double braces becomes an interface element. This is the language of that markup - a small one, with its own parameter grammar, its own escaping and its own rules for finding the function a widget should call.

Widgets that collect a value also feed the layer’s calculation, which is what makes a Mappia map interactive: a slider moved by the user re-runs the arithmetic over the pixels. The full parameter list of every widget is in the API reference under Tools; this page is the language and the behaviour around it.

Use these tools inside a layer’s descriptionHtml. Parameters are pipe-separated:

{{toolName|param=value|other=value}}

HTML and multiple tools can be concatenated:

descriptionHtml:
  '<p>Intro</p>' +
  '{{label|text=Year:}}' +
  '{{slider|id=year|minValue=2000|maxValue=2020|value=2010}}' +
  '{{button|id=run|text=Run|handler=onRun}}'

Open the query panel with paramsButtonConfig: [{ type: 'query', pressed: true }] when the user must see the tools.

Input order: tools that act as inputs (slider, textfield, combobox, timeline, zoomlevel, pickpoint, hoverpixel, summedarea, areaintegral, loadcsv, loadjson, filefield, inputmanager, window) feed expression / beforeCalc / afterCalc as inputs[0], inputs[1], … in markup order. When they have an id they are also inputs.id[ID] — on the page only (beforeCalc, onInputsReady, afterCalc, functions): inside expression the inputs arrive as a JSON copy of the array, which drops the id map, so expression must use positions. button, checkbox, label, legendhtml, opacityslider, livecomposedsplit are not inputs.

Callback names (handler=, toggleHandler=, runOnClick=, runOnHover=, onMark=, onPlayToggle=, _onChange=, …) resolve in this order: (1) layer.functions[name] → (2) a global window[name] (from setQueryGlobalProperties) → (3) the text itself evaluated as a function (a full function(){…} or a bare statement body). this inside is the layer (exceptions per widget below). {{combobox|onSelect=}} skips step 2. Prefer functions: { onRun: function() { … } } on the layer.


Catalog (all Mark.create tags)

TagPurposeKey params (see api.json → Tools → group for the full list)
labelStatic text / HTMLtext, html (verbatim up to next \|), cls, style, forId
buttonClick / toggle actionid, text, handler / toggleHandler, enableToggle, pressed, hidden. toggle= is an alias moved to toggleHandler; with enableToggle=true and no toggleHandler, handler becomes the toggle handler
checkboxBoolean switch — not an input: handler(checkbox, checked) runs on every toggle (also once with false when the layer is removed)id, text, checked, handler, labelBefore, iconCls; runtime Ext.getCmp(id).toggle(state?), setBoxLabel(txt)
textfieldSingle-line text / number inputid, value, fieldLabel, isnumeric (validator only — value stays a string), hideLabel, afteredit
comboboxSelect from listid, data ([["value"], …] - one value per entry, shown as is), fieldLabel, editable, hideLabel, onSelect(combo, record, index)
sliderNumeric range inputid, minValue, maxValue, value / values (range), increment, plugins=tip={0}%, gradient, backgroundColors
opacitysliderLayer opacityvalue (only when the layer has no opacity), inverse, complementaryLayer, changeVisibility
legendhtmlInline map legendreverseLegend, filterLayers, legendId, preventClick, useScaleParameter, autoWidth
timelinePlay through style / scenario stepsid, steps, nextStepInterval, preloadTiles, onPlayToggle (must return truthy — a falsy return vetoes the play/stop), fieldLabel; many runtime methods (setSteps, getValue, startAnimationStep, …)
livecomposedsplitSplit-screen compare of two composed stylesdisplayNames, layerNames, baseName, leftDefault, rightDefault, id, layout (compact|inline)
windowFloating Ext windowid, title, text/html, startVisible, width, height, x, y, items, onBeforeHide, ignoreVisibility
filefieldLocal file pickerid, fieldLabel, ignoreUpdate, _on<event>= for any Ext field event in eventNames (_onChange=onSelectFile, case-insensitive)
loadcsvLoad remote CSV → CsvTableid, url, cors, removeEmptyLines; trim is accepted but ignored by the parser (trim cells yourself)
loadjsonLoad remote JSONid, url, cors
pickpointClick map → feature attributes per layerid, checked, onefeature, geometryColor, onMark (≡ runOnClick; onMark wins), runOnHover, lat/lon, markLayerInd, notify, unselect
hoverpixelPixel value under cursor / on clickid, text, runOnHover, runOnClick, runOnHoverOutside, runOnClickOutside, checked, notify — moving/clicking does not recalculate the layer
summedareaDraw polygon → sum raster valuesid, text, runOnClick(layersValues, inputs, feature), notify, unselect. Its inputs.id[ID] value is never filled — use the callback
areaintegralTwo clicks → rectangle sum via summed-area (integral) mapsid, text, runOnClick(layersValues, inputs, boundingBox, pixel, lastInfo), notify, unselect, iconCls, labelBefore. Was broken for a long time - it registered its input under a name that did not exist and never set lastInfo - and now works
inputmanagerNamed bag of values for callbacksid; runtime getValue, setValues(obj, cancelUpdate, local), setDefaultValues(obj, local), forceRecalc()
zoomlevelHidden input = current map zoomid (required; renders nothing)

Also allowed in descriptionHtml: raw HTML (not a {{…}} tag).


Markup syntax (all tools) — api.json → Tools → MarkupSyntax

RuleEffect
param=valueNumbers become JS numbers; param= (empty) is ""; anything else stays a string (true is the string "true", truthy). Arrays/objects (data, steps, values, filterLayers) are parsed by the tool as JSON
param=falseRewritten to param= (falsy) — the only way to switch off a default-true flag (unselect=false, notify=false)
\|isnumeric\|Bare key = "", except the special keys (isnumeric)
a=b=cNested object {a: {b: c}} — plugins=tip={0}% (slider tip), scope=getid=ID
\=Literal = inside a value (URLs with query strings); double the backslash inside a JS string. html= is exempt (verbatim)
cls=Repeating cls concatenates (no separator); other repeated keys keep the last value
getid=IDReference an element created earlier in the same description; getid=ID\|getid=on_<event>=<code> attaches an Ext listener to it (this = that element; evaluated directly, no functions/globals lookup)
function=<body>A function from raw text (param=function=<body> to assign it); rarely needed — callback params already accept names or inline text

Input values — what inputs.id[ID] holds and what triggers a recalculation

From the <Widget>.value entries in api.json (read the entry for details):

Widgetinputs.id[ID] holdsLayer recalculates on
slidernumber, or [lower, upper] with valuesslider change (thumb released / set from code)
textfieldthe raw text (string, even with isnumeric — parseFloat it)every keyup
comboboxselected data value as a string (first entry initially)select
timelinekey of the current step (first element of the steps entry, string)change (thumb moved, animation) — after the step style is applied
zoomlevelcurrent zoom (number)map moveend, only when the zoom changed
loadcsvExtjsUtils.CSV.CsvTable (header = row 0); undefined until loadedresource waitend — the layer waits, so beforeCalc can rely on it
loadjsonparsed JSON (object/array; [] on empty body)resource waitend
pickpointthe PointAttributeManager (getAttributes(mapIndex), searchAttribute, getPointPos, getLastEvent, removeAll, …) — holds map features: extract plain values in beforeCalc, never send to expressiononmark (after every click, once all layers’ feature info arrived)
hoverpixellastInfo {click: {lon, lat}, hover: {lon, lat}} (EPSG:4326, null before first event; updated in place, so always current when read)nothing — registered on forceupdatelayer, which nothing fires; react in runOnClick/runOnHover
summedareaan array meant for the last sums — stays empty (never filled); use runOnClickforceupdatelayer of the switch (Ext.getCmp(id).items.get(0).forceUpdateLayer())
areaintegrallayersValues of the last rectangle (one sum per inner layer; updated in place)forceupdatelayer — not fired by the tool; call forceUpdateLayer() inside runOnClick
inputmanagerthe manager (getValue, setValues, setDefaultValues, forceRecalc, .global bucket). Only global reaches expression; local=true values may hold DOM/Ext objectsits change — fired by setValues (unless cancelUpdate) and forceRecalc
filefielda getter function: inputs.id[ID]() → the File or undefinedinput change (new file) unless ignoreUpdate=true
windowhandle {getIds, getWindow, getButton, getContainer} (components — not for expression), stored under the button’s id: inputs.id[btnID], not the markup id (set btnID to know it)once, on window afterrender
opacityslidernot an input (value = initial opacity %)—

Caveats worth repeating: LoadCsv.trim is a no-op; SummedArea.value is never filled; Hoverpixel never recalculates by itself (lastInfo is still always current); Timeline.onPlayToggle must return truthy (a bare statement body returns true).


How to choose a tool

NeedPrefer
Show text / HTMLlabel or raw HTML
User runs a named functionbutton (+ functions)
Numeric parameter for expressionslider or textfield\|isnumeric=true
Discrete choicecombobox\|data=…
Boolean switch that runs codecheckbox (+ handler; store the state with an inputmanager if expression needs it)
Opacity controlopacityslider
Show legend in panellegendhtml
Animate styles over timetimeline
Compare two composed styles livelivecomposedsplit
Click map for value / geometrypickpoint
Continuous pixel readhoverpixel
Area sum / integralsummedarea / areaintegral
Upload local filefilefield
Fetch remote CSV/JSONloadcsv / loadjson
Extra floating UIwindow

Minimal patterns (copy / adapt)

Button + handler

{
  title: 'Demo',
  name: 'CSR:estados',
  source: 'calculate',
  visibility: true,
  paramsButtonConfig: [{ type: 'query', pressed: true }],
  descriptionHtml: '{{button|id=say_hi|text=Say hi|handler=onHi}}',
  functions: {
    onHi: function() {
      ExtjsUtils.ALERTIFY.log('hi');
    }
  }
}

Slider as expression input

{
  title: 'Threshold',
  name: 'CSR:altimetria',
  source: 'calculate',
  visibility: true,
  paramsButtonConfig: [{ type: 'query', pressed: true }],
  descriptionHtml: '{{slider|id=thr|minValue=0|maxValue=100|value=50}}',
  expression: function(layerVals, inputs) {
    var thr = inputs[0];
    return layerVals[0] > thr ? 1 : undefined;
  }
}

Combobox

{{combobox|id=month|fieldLabel=Month|data=[["Jan"],["Feb"]]|editable=false}}

data is a list of one-element arrays: each value is both what the list shows and what the input holds. A second element (["01", "Jan"]) is ignored - the store has a single value field.

Timeline

{{timeline|id=lu_tl|nextStepInterval=1000|steps=[["01","January"],["02","February"]]}}

Pairs with composed / calculate layers that change styles per step. See api.json → Timeline (onPlayToggle, preloadTiles, …).

Pickpoint / hoverpixel

{{pickpoint|id=pick|fieldLabel=Pick|checked=false|onefeature=true|onMark=onPicked}}
{{hoverpixel|id=hp|text=Value|runOnHover=onHoverValue|runOnClick=onClickValue}}
functions: {
  onPicked: function (evt) { /* evt.type 'add'|'remove', evt.features[layerIdx][i].data; this = the pickpoint checkbox, this.value = PointAttributeManager */ },
  onHoverValue: function (layerVals, inputs, coordinates, mouseEvt, lastCoordinates) { /* this = layer */ },
  onClickValue: function (layerVals, inputs, coordinates, clickEvt, lastCoordinates) { /* this = layer */ }
}

pickpoint recalculates the layer after every click (onmark); hoverpixel never does — use the callbacks (they receive the map values) or force a recalculation.

Checkbox (handler, not an input)

{{checkbox|id=show_details|text=Show details|checked=true|handler=onToggleDetails}}{{inputmanager|id=state}}
functions: {
  onToggleDetails: function (checkbox, checked) {
    this.getInputs().id["state"].setValues({ details: checked }); // recalculates the layer
  }
}

Area integral (two-click rectangle; needs maps published with the integral operation)

{{areaintegral|id=integral_tool|text=Sum a rectangle|runOnClick=onAreaSummed}}
functions: {
  onAreaSummed: function (layersValues, inputs, boundingBox, pixel, lastInfo) {
    // boundingBox = [minLon, minLat, maxLon, maxLat] (EPSG:4326); lastInfo = {first:{lon,lat}, second:{lon,lat}}
    ExtjsUtils.ALERTIFY.log("Sum: " + layersValues[0]);
    Ext.getCmp("integral_tool").items.get(0).forceUpdateLayer(); // only if beforeCalc must see inputs.id["integral_tool"]
  }
}

summedarea is the free-polygon sibling: runOnClick(layersValues, inputs, feature); its inputs value is never filled.

Opacity + legend

{{opacityslider}}{{legendhtml|reverseLegend=false}}

Live composed split

Requires a calculated layer built from two styles of the same map (often the same name twice with two styles entries). The parameters are listed in the API reference under Tools -> LiveComposedSplit.

{{livecomposedsplit|id=split1|displayNames=A,B|layerNames=style_a,style_b|baseName=CSR:estados|leftDefault=A|rightDefault=B}}

Window

{{window|id=help_win|title=Help|text=Read me|startVisible=false|width=320|height=200}}

File / CSV / JSON

{{filefield|fieldLabel=Load SHP|id=loadshp|_onChange=onSelectFile}}
{{loadcsv|id=tbl|url=https://example.com/data.csv?v\=2|removeEmptyLines=true|cors=true}}
{{loadjson|id=cfg|url=https://example.com/config.json}}

With stable ids, the layer’s callbacks read inputs.id["tbl"] (a CSV table object), inputs.id["cfg"] (parsed JSON) and inputs.id["loadshp"]() (the selected file). When each of these becomes available, and why a layer waits for them, is described in the execution model.

Zoom level (hidden input)

{{zoomlevel|id=z}}

inputs.id["z"] = current zoom (number); refreshed when the map stops moving, and it recalculates the layer only when the zoom actually changed.

Timeline with a play veto

{{timeline|id=tl|steps=[["1990","1990"],["2000","2000"]]|onPlayToggle=onPlay}}
functions: {
  onPlay: function (pressed, layer, timeline, playBtn) { return !window.stillLoading; } // falsy = veto
}

Authoring checklist

  1. Put tools only in descriptionHtml (not as top-level QUERY keys).
  2. Use the exact tag from the catalog (livecomposedsplit, not LiveComposedSplit).
  3. Give interactive tools stable ids when other code or inputs must find them.
  4. Wire handler / toggleHandler / runOnClick names to functions: { … } on the same layer (regular function, not arrows — this is the layer).
  5. Remember input order for expression(layerVals, inputs), and that only plain data reaches expression.
  6. For a side-by-side comparison use livecomposedsplit; there is no second split API.
  7. Prefer api.json → Tools → <Name> when a parameter’s type/default is unclear; <Name>.value says what the input holds.
  8. Panels with several widgets, the layer-row buttons (paramsButtonConfig) and page chrome belong to the layer and group model.
  9. An unknown tag fails silently: the element is simply not created, and the console shows {{MARKUP}} INVALID OBJECT NAME.

Generated API entries for this chapter

Every property and function named here is listed, with its type, default and example, in the generated API reference: