Display offline live alarm layers on map

🟡 Intermediate

map alarm script

This example keeps the map offline with MapLibre and a Protomaps archive, while the displayed features are streamed live from osp-alarms. The geometry is read from additionalData.geometry and icon selection is driven by additionalData.markerType.

  • Reuse PMTiles and icon assets from the offline maps example via module.resources.

  • Declare SEVERITY and ICON alarm-backed layers with alarm views and configurable filters.

  • Create alarms from a map click or from the alarm table toolbar using a JavaScript script and a form prompt.

  • Switch between alarm filters (all / critical only) to confirm that both map layers update in real time.

git checkout origin/osp-alarms-configuration .
git checkout origin/osp-web-configuration .
git checkout origin/osp-maps-configuration .
git checkout origin/osp-scripts-configuration .
git checkout origin/osp-variables-configuration .
git checkout origin/example-maps-offline-live-alarms .

Learning goals

  • How to configure ALARMS sources for both SEVERITY and ICON layers.

  • How to reuse offline PMTiles and shared icon assets from example-maps-basic-offline-maps.

  • How to create alarms from the map or the alarm table so live alarm-backed layers update immediately.

1. Reuse the offline tiles and icon assets

The example inherits its PMTiles file and PNG icons from example-maps-basic-offline-maps. Those assets are already exposed through module.resources and reused by the dashboard below.

modules/web/web-1/module.resources (from example-maps-basic-offline-maps)
{
    "resources": [
        {
            "source": "root/feature/protomaps/assets/CH-fr-Fribourg.pmtiles",
            "destination": "osp/resources/tiles/fr.pmtiles"
        },
        {
            "source": "root/feature/protomaps/assets/vd.pmtiles",
            "destination": "osp/resources/tiles/vd.pmtiles"
        },
        {
            "source": "root/feature/protomaps/assets/sdn_logo.png",
            "destination": "osp/resources/images/sdn_logo.png"
        },
        {
            "source": "root/feature/protomaps/assets/fire.png",
            "destination": "osp/resources/icons/fire.png"
        },
        {
            "source": "root/feature/protomaps/assets/maintenance.png",
            "destination": "osp/resources/icons/maintenance.png"
        }
    ]
}

2. Declare the alarm view and filters

The alarm view exposes the fields used by both the live table and the map layers, including additionalData.geometry and additionalData.markerType.

root/alarms/views/live_map/view.ospp
{
    "name": "Live map alarms",
    "description": "Alarm view for the offline live alarm map example",
    "liveColumns": [
        {
            "name": "Serial",
            "field": "serial"
        },
        {
            "name": "Summary",
            "field": "summary"
        },
        {
            "name": "Source",
            "field": "source"
        },
        {
            "name": "Location",
            "field": "location"
        },
        {
            "name": "Severity",
            "field": "severity"
        },
        {
            "name": "Highest severity",
            "field": "highestSeverity"
        },
        {
            "name": "Count",
            "field": "count"
        },
        {
            "name": "Marker type",
            "field": "additionalData.markerType"
        },
        {
            "name": "Geometry",
            "field": "additionalData.geometry"
        },
        {
            "name": "Tags",
            "field": "tags"
        },
        {
            "name": "Last occurrence",
            "field": "lastTimestamp"
        }
    ],
    "historyColumns": [
        {
            "name": "Serial",
            "field": "serial"
        },
        {
            "name": "Summary",
            "field": "summary"
        },
        {
            "name": "Source",
            "field": "source"
        },
        {
            "name": "Location",
            "field": "location"
        },
        {
            "name": "Severity",
            "field": "severity"
        },
        {
            "name": "Count",
            "field": "count"
        },
        {
            "name": "Marker type",
            "field": "additionalData.markerType"
        },
        {
            "name": "Geometry",
            "field": "additionalData.geometry"
        },
        {
            "name": "Operation time",
            "field": "operationTime"
        }
    ]
}

The layers declare two alarm filter ids: root.alarms.filters.all and root.alarms.filters.critical_only. The first uses an empty query, while the second keeps alarms whose highestSeverity is 500 or above.

root/alarms/filters/critical_only/filter.alarms
{
    "moduleId": "modules.alarms.alarms-1",
    "query": {
        "highestSeverity": {
            "$gte": 500
        }
    }
}

3. Create the alarm-backed layers

The severity layer streams live alarms directly from the alarms module and reads their geometry from additionalData.geometry.

root/layers/alarm_severity/layer.ospp
{
    "name": "Live alarm severity",
    "description": "Live alarm severity points streamed directly from the alarms module",
    "layerType": "SEVERITY",
    "valueType": "POINT",
    "clusteringType": "NONE",
    "mapSources": [
        {
            "type": "ALARMS",
            "geometry": "additionalData.geometry",
            "view": "root.alarms.views.live_map",
            "filter": [
                "root.alarms.filters.all",
                "root.alarms.filters.critical_only"
            ]
        }
    ]
}

The icon layer reuses the same view and filter ids, but also declares the allowed icons and displayRules. visibleExpression is evaluated against alarm fields, so additionalData.markerType decides whether the fire or maintenance icon is rendered.

root/layers/alarm_icons/layer.ospp
{
    "name": "Live alarm icons",
    "description": "Live alarm icon points streamed directly from the alarms module",
    "layerType": "ICON",
    "valueType": "POINT",
    "icons": [
        "fire",
        "maintenance"
    ],
    "mapSources": [
        {
            "type": "ALARMS",
            "geometry": "additionalData.geometry",
            "view": "root.alarms.views.live_map",
            "filter": [
                "root.alarms.filters.all",
                "root.alarms.filters.critical_only"
            ],
            "displayRules": [
                {
                    "iconName": "fire",
                    "visibleExpression": "additionalData.markerType == 'fire'"
                },
                {
                    "iconName": "maintenance",
                    "visibleExpression": "additionalData.markerType == 'maintenance'"
                }
            ]
        }
    ]
}

The linked layer.maps and layer.web files only bind those layers to modules.maps.maps-1 and modules.web.web-1.

4. Configure the offline dashboard and menus

The dashboard enables both layer ids, points the widget to the offline PMTiles archive, reuses the inherited icon resources and shows an AlarmTable configured with the same live view and default filter.

root/maps/dashboard.view
{
    "configuration": [
        {
            "type": "BoxLayout",
            "id": "offline_live_alarms_layout",
            "title": "",
            "boxLayoutWidgetSettings": {
                "orientation": "Vertical",
                "components": [
                    {
                        "component": {
                            "id": "offline_live_alarms_map",
                            "type": "Maps",
                            "title": "",
                            "menuReferences": [
                                "root.maps.menu"
                            ],
                            "mapsWidgetSettings": {
                                "layerOptions": [
                                    {
                                        "layerId": "root.layers.alarm_severity",
                                        "activatedByDefault": true
                                    },
                                    {
                                        "layerId": "root.layers.alarm_icons",
                                        "activatedByDefault": true
                                    }
                                ],
                                "protomapSource": "/osp/resources/tiles/fr.pmtiles",
                                "library": "MapLibre",
                                "icons": [
                                    {
                                        "path": "osp/resources/icons/fire.png",
                                        "name": "fire"
                                    },
                                    {
                                        "path": "osp/resources/icons/maintenance.png",
                                        "name": "maintenance"
                                    }
                                ],
                                "iconPriority": [
                                    "fire",
                                    "maintenance"
                                ],
                                "centerPosition": [
                                    7.139734,
                                    46.806418
                                ],
                                "zoomLevel": 14,
                                "maxBounds": [
                                    [
                                        7.097176690737683,
                                        46.77751158309039
                                    ],
                                    [
                                        7.1740675580062145,
                                        46.82261507130144
                                    ]
                                ],
                                "minZoom": 14
                            }
                        },
                        "size": 0.58
                    },
                    {
                        "component": {
                            "type": "AlarmTable",
                            "id": "offline_live_alarms_table",
                            "title": "Live alarms",
                            "menuReferences": [
                                "root.alarms.menu"
                            ],
                            "alarmWidgetSettings": {
                                "defaultFilter": "root.alarms.filters.all",
                                "defaultView": "root.alarms.views.live_map"
                            }
                        },
                        "size": 0.42
                    }
                ]
            }
        }
    ],
    "layout": {
        "lg": [
            {
                "w": 12,
                "h": 6,
                "x": 0,
                "y": 0,
                "i": "offline_live_alarms_layout"
            }
        ]
    },
    "breakpoints": {
        "lg": 1200,
        "md": 996,
        "sm": 768,
        "xs": 480,
        "xxs": 0
    },
    "cols": {
        "lg": 12,
        "md": 10,
        "sm": 6,
        "xs": 4,
        "xxs": 2
    },
    "rowHeight": 150
}

The map context menu captures click coordinates, opens the form and triggers root.maps.actions.run_script.

root/maps/menu/menu.web
{
    "moduleId": [
        "modules.web.web-1"
    ],
    "name": "Offline live alarms map menu",
    "description": "Create an alarm from the offline map context menu",
    "menus": [
        {
            "label": "Create alarm here",
            "icon": "add_alert",
            "context": [
                {
                    "type": [
                        "Maps"
                    ],
                    "scopes": [
                        "Click"
                    ],
                    "action": "root.maps.actions.run_script",
                    "input": {
                        "scriptId": {
                            "expression": "'root.script.create_alarm'"
                        },
                        "form": {
                            "prompt": {
                                "type": "FORM",
                                "formId": "root.maps.create_alarm.form",
                                "formSave": false,
                                "labels": {
                                    "title": "Create alarm",
                                    "message": "Confirm and edit the alarm fields",
                                    "save": "Create",
                                    "cancel": "Cancel"
                                },
                                "data": {
                                    "summary": {
                                        "expression": "'Alarm from offline map click'"
                                    },
                                    "source": {
                                        "expression": "'maps.offline-live-alarms'"
                                    },
                                    "longitude": {
                                        "expression": "map.point.lngLat.lng"
                                    },
                                    "latitude": {
                                        "expression": "map.point.lngLat.lat"
                                    },
                                    "location": {
                                        "expression": "'lng=' + map.point.lngLat.lng.toFixed(6) + ', lat=' + map.point.lngLat.lat.toFixed(6)"
                                    },
                                    "severity": {
                                        "expression": "300"
                                    },
                                    "markerType": {
                                        "expression": "'fire'"
                                    },
                                    "tags": {
                                        "expression": "'maps,offline,manual'"
                                    }
                                }
                            }
                        }
                    },
                    "output": [
                        {
                            "operation": "snackbar",
                            "input": {
                                "message": {
                                    "expression": "${result.success} ? 'Alarm created from offline map context menu' : 'Alarm creation failed from offline map context menu: ' + (${result.message} || ${result.error} || 'unknown error')"
                                },
                                "variant": {
                                    "expression": "${result.success} ? 'success' : 'error'"
                                }
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

The alarm table toolbar reuses the same script flow, but lets operators enter coordinates manually from the form.

root/alarms/menu/menu.web
{
    "moduleId": [
        "modules.web.web-1"
    ],
    "name": "Offline live alarms table menu",
    "description": "Create an alarm from the offline live alarms table toolbar",
    "menus": [
        {
            "label": "Create alarm",
            "icon": "add_alert",
            "context": [
                {
                    "type": "AlarmTable",
                    "scopes": [
                        "Toolbar"
                    ],
                    "condition": "true",
                    "action": "root.alarms.actions.run_script",
                    "input": {
                        "scriptId": {
                            "expression": "'root.script.create_alarm'"
                        },
                        "form": {
                            "prompt": {
                                "type": "FORM",
                                "formId": "root.maps.create_alarm.form",
                                "formSave": false,
                                "labels": {
                                    "title": "Create alarm",
                                    "message": "Enter coordinates and alarm fields",
                                    "save": "Create",
                                    "cancel": "Cancel"
                                },
                                "data": {
                                    "summary": {
                                        "expression": "'Alarm from offline alarm table'"
                                    },
                                    "source": {
                                        "expression": "'maps.offline-live-alarms'"
                                    },
                                    "severity": {
                                        "expression": "300"
                                    },
                                    "markerType": {
                                        "expression": "'fire'"
                                    },
                                    "tags": {
                                        "expression": "'maps,offline,manual'"
                                    }
                                }
                            }
                        }
                    },
                    "output": [
                        {
                            "operation": "snackbar",
                            "input": {
                                "message": {
                                    "expression": "${result.success} ? 'Alarm created from offline alarm table' : 'Alarm creation failed from offline alarm table: ' + (${result.message} || ${result.error} || 'unknown error')"
                                },
                                "variant": {
                                    "expression": "${result.success} ? 'success' : 'error'"
                                }
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

Both root.maps.actions.run_script and root.alarms.actions.run_script are small RUN_SCRIPT bindings to modules.web.web-1.

5. Configure the form and the script

The form schema defines the fields used to create the alarm, including longitude, latitude, severity, marker type and optional serial.

root/maps/create_alarm/form/schema.ospp
{
    "schema": {
        "type": "object",
        "properties": {
            "summary": {
                "type": "string"
            },
            "source": {
                "type": "string"
            },
            "longitude": {
                "type": "number"
            },
            "latitude": {
                "type": "number"
            },
            "location": {
                "type": "string"
            },
            "severity": {
                "type": "number"
            },
            "markerType": {
                "type": "string",
                "enum": [
                    "fire",
                    "maintenance"
                ]
            },
            "tags": {
                "type": "string"
            },
            "serial": {
                "type": "string"
            }
        }
    }
}

The form layout and defaults are declared in form.web. The menus prefill it either from the map click or from toolbar defaults.

root/maps/create_alarm/form/form.web
{
    "moduleId": [
        "modules.web.web-1"
    ],
    "name": "Offline live alarms create form",
    "description": "",
    "bindings": {},
    "rights": [],
    "schema": "root.maps.create_alarm.form",
    "ui": {
        "type": "VerticalLayout",
        "elements": [
            {
                "type": "Control",
                "label": "Summary",
                "scope": "#/properties/summary"
            },
            {
                "type": "Control",
                "label": "Source",
                "scope": "#/properties/source"
            },
            {
                "type": "Control",
                "label": "Longitude",
                "scope": "#/properties/longitude"
            },
            {
                "type": "Control",
                "label": "Latitude",
                "scope": "#/properties/latitude"
            },
            {
                "type": "Control",
                "label": "Location",
                "scope": "#/properties/location"
            },
            {
                "type": "Control",
                "label": "Severity",
                "scope": "#/properties/severity"
            },
            {
                "type": "Control",
                "label": "Marker type",
                "scope": "#/properties/markerType"
            },
            {
                "type": "Control",
                "label": "Tags",
                "scope": "#/properties/tags"
            },
            {
                "type": "Control",
                "label": "Serial (optional)",
                "scope": "#/properties/serial"
            }
        ]
    },
    "initialValue": {
        "source": "maps.offline-live-alarms",
        "severity": 300,
        "markerType": "fire",
        "tags": "maps,offline,manual"
    },
    "submit": {
        "destination": "Request",
        "type": "Deferred"
    }
}

The script id root.script.create_alarm is declared through detached.scripts and points to the JavaScript implementation.

root/script/create_alarm/detached.scripts
{
    "moduleId": "modules.scripts.scripts-1",
    "accessedValues": [],
    "scheduledExecutions": [],
    "sourceFile": "root/script/create_alarm/create_alarm.js"
}

The script builds the alarm payload, writes the clicked coordinates into additionalData.geometry, stores the icon selector in additionalData.markerType, tries alarms.create() and falls back to alarms.insert() when needed.

root/script/create_alarm/create_alarm.js
const DEFAULT_SEVERITY = 300;
const DEFAULT_MARKER_TYPE = "fire";

try {
    main();
} catch (error) {
    const message = !!error && !!error.message ? String(error.message) : String(error);
    log.error("Offline live alarms map script failed with unexpected error [{}].", message);
    execution.reportFailure(message);
}

function main() {
    const parameters = Array.isArray(trigger.parameters) && trigger.parameters.length > 0
        ? trigger.parameters[0]
        : {};
    const form = typeof parameters.form === "object" && parameters.form !== null ? parameters.form : {};
    const longitude = parseCoordinate(form.longitude, parameters.longitude);
    const latitude = parseCoordinate(form.latitude, parameters.latitude);

    if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
        log.error("Missing map coordinates in script parameters [{}].", JSON.stringify(parameters));
        execution.reportFailure("Missing map coordinates in script parameters.");
    }

    const severity = parseSeverity(form.severity);
    const markerType = parseMarkerType(form.markerType);
    const summary = toText(form.summary, "Alarm from offline map click");
    const source = toText(form.source, "maps.offline-live-alarms");
    const location = toText(form.location, `lng=${longitude.toFixed(6)}, lat=${latitude.toFixed(6)}`);
    const serial = buildSerial(form.serial);
    const tags = parseTags(form.tags);

    log.info(
        "Create offline live alarms example alarm with serial [{}], severity [{}], marker type [{}], location [{}].",
        serial,
        severity,
        markerType,
        location
    );

    const alarm = alarms.newAlarmBuilder(serial, severity)
        .setSummary(summary)
        .setSource(source)
        .setLocation(location)
        .addTags(tags)
        .addAdditionalData("geometry", {
            type: "Point",
            coordinates: [longitude, latitude]
        })
        .addAdditionalData("markerType", markerType)
        .build();

    const createResult = alarms.create(alarm);
    if (createResult && createResult.success === true) {
        log.info("Alarm creation succeeded from offline live alarms script [{}] with create().", serial);
        execution.reportResult("Alarm created", {
            serial: serial,
            longitude: longitude,
            latitude: latitude,
            markerType: markerType
        });
    }

    log.warn(
        "Alarm create() failed for offline live alarms script [{}], trying insert(). create result [{}].",
        serial,
        JSON.stringify(createResult)
    );

    const insertResult = alarms.insert(alarm);
    if (!insertResult || insertResult.success !== true) {
        const createMessage = !!createResult && !!createResult.message ? createResult.message : JSON.stringify(createResult);
        const insertMessage = !!insertResult && !!insertResult.message ? insertResult.message : JSON.stringify(insertResult);
        const failureMessage = `create failed [${createMessage}] / insert failed [${insertMessage}]`;
        log.error("Alarm creation failed from offline live alarms script [{}] cause [{}].", serial, failureMessage);
        execution.reportFailure(failureMessage);
    }

    log.info("Alarm creation succeeded from offline live alarms script [{}] with insert().", serial);
    execution.reportResult("Alarm created", {
        serial: serial,
        longitude: longitude,
        latitude: latitude,
        markerType: markerType
    });
}

function toText(value, fallback) {
    if (value === undefined || value === null) {
        return fallback;
    }

    const normalized = String(value).trim();
    return normalized.length > 0 ? normalized : fallback;
}

function parseSeverity(rawValue) {
    const parsed = Number(rawValue);
    return Number.isFinite(parsed) ? parsed : DEFAULT_SEVERITY;
}

function parseCoordinate(rawValue, fallbackValue) {
    const parsed = Number(rawValue);
    if (Number.isFinite(parsed)) {
        return parsed;
    }

    const fallback = Number(fallbackValue);
    return Number.isFinite(fallback) ? fallback : NaN;
}

function parseMarkerType(rawValue) {
    const markerType = toText(rawValue, DEFAULT_MARKER_TYPE);
    return markerType === "maintenance" ? "maintenance" : DEFAULT_MARKER_TYPE;
}

function buildSerial(serialValue) {
    const provided = toText(serialValue, "");
    if (provided.length > 0) {
        return provided;
    }

    return `offline-live-alarm-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
}

function parseTags(rawTags) {
    if (Array.isArray(rawTags)) {
        return rawTags
            .map((tag) => String(tag).trim())
            .filter((tag) => tag.length > 0);
    }

    if (typeof rawTags === "string") {
        return rawTags
            .split(",")
            .map((tag) => tag.trim())
            .filter((tag) => tag.length > 0);
    }

    return [];
}

6. Validate the behavior

  1. Open the Offline live alarms dashboard.

  2. Click on the map and choose Create alarm here, or use the Create alarm action from the alarm table toolbar.

  3. Submit the form and confirm the success snackbar.

  4. Verify that the new alarm appears in the table and immediately shows up on the map through root.layers.alarm_severity and root.layers.alarm_icons.

  5. Switch between the All and Critical only filters to confirm that both alarm-backed layers reuse the alarm filter ids declared in layer.ospp.