The widget markup language
The double-brace markup that puts sliders, buttons, pickers and loaders inside a layer’s panel - every tag, the parameter grammar, how callbacks are resolved and what each widget’s value holds.
Mappia reference, section “Tools”. Page: https://mappia.earth/reference/markup-widgets/ Generated API entries: MarkupSyntax, Slider, Textfield, Combobox, Button, Checkbox, Label, Window, Timeline, PickPoint, Hoverpixel, SummedArea, AreaIntegral, InputManager, ZoomLevel, LoadCsv, LoadJson, FileField, LegendHtml, OpacitySlider, LiveComposedSplit, LayersProperties.descriptionHtml (https://mappia.earth/assets/api.json)
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:
HTML and multiple tools can be concatenated:
descriptionHtml:
'<p>Intro</p>' +
'' +
'' +
''
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). `` skips step 2. Prefer functions: { onRun: function() { … } } on the layer.
Catalog (all Mark.create tags)
| Tag | Purpose | Key params (see api.json → Tools → group for the full list) |
|---|---|---|
label | Static text / HTML | text, html (verbatim up to next \|), cls, style, forId |
button | Click / toggle action | id, text, handler / toggleHandler, enableToggle, pressed, hidden. toggle= is an alias moved to toggleHandler; with enableToggle=true and no toggleHandler, handler becomes the toggle handler |
checkbox | Boolean 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) |
textfield | Single-line text / number input | id, value, fieldLabel, isnumeric (validator only — value stays a string), hideLabel, afteredit |
combobox | Select from list | id, data ([["value"], …] - one value per entry, shown as is), fieldLabel, editable, hideLabel, onSelect(combo, record, index) |
slider | Numeric range input | id, minValue, maxValue, value / values (range), increment, plugins=tip={0}%, gradient, backgroundColors |
opacityslider | Layer opacity | value (only when the layer has no opacity), inverse, complementaryLayer, changeVisibility |
legendhtml | Inline map legend | reverseLegend, filterLayers, legendId, preventClick, useScaleParameter, autoWidth |
timeline | Play through style / scenario steps | id, steps, nextStepInterval, preloadTiles, onPlayToggle (must return truthy — a falsy return vetoes the play/stop), fieldLabel; many runtime methods (setSteps, getValue, startAnimationStep, …) |
livecomposedsplit | Split-screen compare of two composed styles | displayNames, layerNames, baseName, leftDefault, rightDefault, id, layout (compact|inline) |
window | Floating Ext window | id, title, text/html, startVisible, width, height, x, y, items, onBeforeHide, ignoreVisibility |
filefield | Local file picker | id, fieldLabel, ignoreUpdate, _on<event>= for any Ext field event in eventNames (_onChange=onSelectFile, case-insensitive) |
loadcsv | Load remote CSV → CsvTable | id, url, cors, removeEmptyLines; trim is accepted but ignored by the parser (trim cells yourself) |
loadjson | Load remote JSON | id, url, cors |
pickpoint | Click map → feature attributes per layer | id, checked, onefeature, geometryColor, onMark (≡ runOnClick; onMark wins), runOnHover, lat/lon, markLayerInd, notify, unselect |
hoverpixel | Pixel value under cursor / on click | id, text, runOnHover, runOnClick, runOnHoverOutside, runOnClickOutside, checked, notify — moving/clicking does not recalculate the layer |
summedarea | Draw polygon → sum raster values | id, text, runOnClick(layersValues, inputs, feature), notify, unselect. Its inputs.id[ID] value is never filled — use the callback |
areaintegral | Two clicks → rectangle sum via summed-area (integral) maps | id, 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 |
inputmanager | Named bag of values for callbacks | id; runtime getValue, setValues(obj, cancelUpdate, local), setDefaultValues(obj, local), forceRecalc() |
zoomlevel | Hidden input = current map zoom | id (required; renders nothing) |
Also allowed in descriptionHtml: raw HTML (not a `` tag).
Markup syntax (all tools) — api.json → Tools → MarkupSyntax
| Rule | Effect |
|---|---|
param=value | Numbers 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=false | Rewritten 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=c | Nested 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=ID | Reference 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):
| Widget | inputs.id[ID] holds | Layer recalculates on |
|---|---|---|
slider | number, or [lower, upper] with values | slider change (thumb released / set from code) |
textfield | the raw text (string, even with isnumeric — parseFloat it) | every keyup |
combobox | selected data value as a string (first entry initially) | select |
timeline | key of the current step (first element of the steps entry, string) | change (thumb moved, animation) — after the step style is applied |
zoomlevel | current zoom (number) | map moveend, only when the zoom changed |
loadcsv | ExtjsUtils.CSV.CsvTable (header = row 0); undefined until loaded | resource waitend — the layer waits, so beforeCalc can rely on it |
loadjson | parsed JSON (object/array; [] on empty body) | resource waitend |
pickpoint | the PointAttributeManager (getAttributes(mapIndex), searchAttribute, getPointPos, getLastEvent, removeAll, …) — holds map features: extract plain values in beforeCalc, never send to expression | onmark (after every click, once all layers’ feature info arrived) |
hoverpixel | lastInfo {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 |
summedarea | an array meant for the last sums — stays empty (never filled); use runOnClick | forceupdatelayer of the switch (Ext.getCmp(id).items.get(0).forceUpdateLayer()) |
areaintegral | layersValues of the last rectangle (one sum per inner layer; updated in place) | forceupdatelayer — not fired by the tool; call forceUpdateLayer() inside runOnClick |
inputmanager | the manager (getValue, setValues, setDefaultValues, forceRecalc, .global bucket). Only global reaches expression; local=true values may hold DOM/Ext objects | its change — fired by setValues (unless cancelUpdate) and forceRecalc |
filefield | a getter function: inputs.id[ID]() → the File or undefined | input change (new file) unless ignoreUpdate=true |
window | handle {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 |
opacityslider | not 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
| Need | Prefer |
|---|---|
| Show text / HTML | label or raw HTML |
| User runs a named function | button (+ functions) |
Numeric parameter for expression | slider or textfield\|isnumeric=true |
| Discrete choice | combobox\|data=… |
| Boolean switch that runs code | checkbox (+ handler; store the state with an inputmanager if expression needs it) |
| Opacity control | opacityslider |
| Show legend in panel | legendhtml |
| Animate styles over time | timeline |
| Compare two composed styles live | livecomposedsplit |
| Click map for value / geometry | pickpoint |
| Continuous pixel read | hoverpixel |
| Area sum / integral | summedarea / areaintegral |
| Upload local file | filefield |
| Fetch remote CSV/JSON | loadcsv / loadjson |
| Extra floating UI | window |
Minimal patterns (copy / adapt)
Button + handler
{
title: 'Demo',
name: 'CSR:estados',
source: 'calculate',
visibility: true,
paramsButtonConfig: [{ type: 'query', pressed: true }],
descriptionHtml: '',
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: '',
expression: function(layerVals, inputs) {
var thr = inputs[0];
return layerVals[0] > thr ? 1 : undefined;
}
}
Combobox
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
Pairs with composed / calculate layers that change styles per step. See api.json → Timeline (onPlayToggle, preloadTiles, …).
Pickpoint / hoverpixel
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)
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)
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
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.
Window
File / CSV / 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)
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
functions: {
onPlay: function (pressed, layer, timeline, playBtn) { return !window.stillLoading; } // falsy = veto
}
Authoring checklist
- Put tools only in
descriptionHtml(not as top-level QUERY keys). - Use the exact tag from the catalog (
livecomposedsplit, notLiveComposedSplit). - Give interactive tools stable
ids when other code or inputs must find them. - Wire
handler/toggleHandler/runOnClicknames tofunctions: { … }on the same layer (regularfunction, not arrows —thisis the layer). - Remember input order for
expression(layerVals, inputs), and that only plain data reachesexpression. - For a side-by-side comparison use
livecomposedsplit; there is no second split API. - Prefer
api.json→ Tools →<Name>when a parameter’s type/default is unclear;<Name>.valuesays what the input holds. - Panels with several widgets, the layer-row buttons (
paramsButtonConfig) and page chrome belong to the layer and group model. - An unknown tag fails silently: the element is simply not created, and the console shows ` INVALID OBJECT NAME`.