Display offline live alarm layers on map
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
SEVERITYandICONalarm-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
ALARMSsources for bothSEVERITYandICONlayers.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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
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.
{
"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.
{
"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.
{
"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.
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
Open the
Offline live alarmsdashboard.Click on the map and choose
Create alarm here, or use theCreate alarmaction from the alarm table toolbar.Submit the form and confirm the success snackbar.
Verify that the new alarm appears in the table and immediately shows up on the map through
root.layers.alarm_severityandroot.layers.alarm_icons.Switch between the
AllandCritical onlyfilters to confirm that both alarm-backed layers reuse the alarm filter ids declared inlayer.ospp.