Display collection-backed layers on map

🟡 Intermediate

map collection

This example uses one collection schema as the source of truth for map items. The STANDARD layer displays all visible points from the collection, while the ICON layer reuses the same documents and adds icon rules based on the category field.

  • Define a collection schema with a nested GeoJSON object at map.geometry for geographic coordinates.

  • Configure a STANDARD layer and an ICON layer both sourced from the same collection via collectionSource.

  • Share the same visible_only filter between the map layers and the CollectionTable widget.

  • Handle live map updates: collection create, update, and delete operations push to the same websocket stream without any widget-specific refresh.

git checkout origin/osp-maps-configuration .
git checkout origin/osp-web-configuration .
git checkout origin/osp-collections-configuration .
git checkout origin/osp-scripts-configuration .
git checkout origin/example-maps-collection-dynamic-layers .

Learning goals

  • How to configure a COLLECTIONS map source.

  • How to stream one schema into both a STANDARD and an ICON layer.

  • How to reuse the same list-based filter definition across the map and the collection table.

  • How to keep the map live with collection updates without changing the widget request contract.

1. Define the collection schema

The collection stores business fields plus the nested GeoJSON object at map.geometry.

root/collections/dynamic_map_items/schema.ospp
{
    "schema": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "category": {
                "type": "string",
                "enum": [
                    "fire",
                    "maintenance"
                ]
            },
            "visibleOnMap": {
                "type": "boolean"
            },
            "map": {
                "type": "object",
                "properties": {
                    "geometry": {
                        "type": "object",
                        "properties": {
                            "type": {
                                "type": "string",
                                "enum": [
                                    "Point"
                                ]
                            },
                            "coordinates": {
                                "type": "array",
                                "minItems": 2,
                                "maxItems": 2,
                                "items": {
                                    "type": "number"
                                }
                            }
                        },
                        "required": [
                            "type",
                            "coordinates"
                        ]
                    }
                },
                "required": [
                    "geometry"
                ]
            }
        },
        "required": [
            "name",
            "category",
            "visibleOnMap",
            "map"
        ]
    },
    "filters": [
        {
            "id": "visible_only",
            "name": "Visible points only"
        }
    ]
}

The visible_only filter metadata is declared in schema.ospp and implemented in schema.collections. The map layers and the table both reference it by id.

root/collections/dynamic_map_items/schema.collections
{
    "moduleId": "modules.collections.collections-1",
    "collectionName": "dynamic_map_items",
    "indexes": [
        {
            "name": "visibleOnMap",
            "index": "{'visibleOnMap': 1}"
        },
        {
            "name": "category",
            "index": "{'category': 1}"
        }
    ],
    "filters": [
        {
            "id": "visible_only",
            "query": "{visibleOnMap: true}"
        }
    ]
}

2. Create the collection-backed layers

The standard layer streams the collection documents by collectionSource, extracts GeoJSON from map.geometry and applies the list of schema filter ids declared in filter.

root/layers/collection_standard/layer.ospp
{
    "name": "Dynamic collection points",
    "description": "Points streamed from the collections module",
    "layerType": "STANDARD",
    "valueType": "POINT",
    "clusteringType": "NONE",
    "mapSources": [
        {
            "type": "COLLECTIONS",
            "collectionSource": "root.collections.dynamic_map_items",
            "geometry": "map.geometry",
            "filter": [
                "visible_only"
            ]
        }
    ]
}

The icon layer reuses the same collection, geometry path and filter list, but also declares the allowed icons and displayRules. visibleExpression is evaluated against the streamed document properties.

root/layers/collection_icons/layer.ospp
{
    "name": "Dynamic collection icons",
    "description": "Point icons streamed from the collections module",
    "layerType": "ICON",
    "valueType": "POINT",
    "icons": [
        "fire",
        "maintenance"
    ],
    "mapSources": [
        {
            "type": "COLLECTIONS",
            "collectionSource": "root.collections.dynamic_map_items",
            "geometry": "map.geometry",
            "filter": [
                "visible_only"
            ],
            "displayRules": [
                {
                    "iconName": "fire",
                    "visibleExpression": "category == 'fire'"
                },
                {
                    "iconName": "maintenance",
                    "visibleExpression": "category == 'maintenance'"
                }
            ]
        }
    ]
}

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

3. Configure the dashboard widgets and icon assets

The dashboard activates both layer ids and declares the PNG icon assets used by the icon layer, following the same module.resources pattern as the standard icon example.

root/dashboard.view
{
    "configuration": [
        {
            "id": "maps-dynamic-collection-widget",
            "type": "Maps",
            "title": "Dynamic collection layers",
            "mapsWidgetSettings": {
                "layerOptions": [
                    {
                        "layerId": "root.layers.collection_standard",
                        "activatedByDefault": true
                    },
                    {
                        "layerId": "root.layers.collection_icons",
                        "activatedByDefault": true
                    }
                ],
                "mapStyle": {
                    "dark": "mapbox://styles/mapbox/dark-v11",
                    "light": "mapbox://styles/mapbox/light-v11"
                },
                "icons": [
                    {
                        "path": "osp/resources/icons/fire.png",
                        "name": "fire"
                    },
                    {
                        "path": "osp/resources/icons/maintenance.png",
                        "name": "maintenance"
                    }
                ],
                "iconPriority": [
                    "fire",
                    "maintenance"
                ],
                "centerPosition": [
                    7.4474,
                    46.948
                ],
                "zoomLevel": 8
            }
        },
        {
            "id": "dynamic-map-items-table",
            "type": "CollectionTable",
            "title": "Visible collection entries",
            "menuReferences": [
                "root.collections.dynamic_map_items.delete.menu"
            ],
            "collectionTableWidgetSettings": {
                "defaultSchemas": [
                    "root.collections.dynamic_map_items"
                ],
                "defaultFilter": "visible_only",
                "pageSize": 50,
                "pageSizes": [
                    50,
                    100,
                    200
                ],
                "resizeMode": "widget",
                "disableToolbarTableRefresh": false,
                "disableSchemaUpdate": false,
                "disableViewUpdate": false,
                "disableFilterUpdate": false,
                "disableToolbar": false,
                "disableToolbarMenu": false,
                "disableSidePanel": false,
                "disableToolbarExport": false,
                "disableToolbarColumnShowHide": false,
                "disableToolbarFilterShowHide": false,
                "disableToolbarSummaryShowHide": false,
                "disableToolbarSearch": false,
                "disableToolbarColumnChooser": false,
                "disableToolbarClearFilter": false
            }
        }
    ],
    "layout": {
        "lg": [
            {
                "w": 12,
                "h": 4,
                "x": 0,
                "y": 0,
                "i": "maps-dynamic-collection-widget"
            },
            {
                "w": 12,
                "h": 5,
                "x": 0,
                "y": 4,
                "i": "dynamic-map-items-table"
            }
        ]
    },
    "breakpoints": {
        "lg": 1200,
        "md": 996,
        "sm": 768,
        "xs": 480,
        "xxs": 0
    },
    "cols": {
        "lg": 12,
        "md": 10,
        "sm": 6,
        "xs": 4,
        "xxs": 2
    },
    "rowHeight": 130
}
modules/web/web-1/module.resources
{
    "resources": [
        {
            "destination": "osp/resources/icons/fire.png",
            "source": "root/layers/assets/fire.png"
        },
        {
            "destination": "osp/resources/icons/maintenance.png",
            "source": "root/layers/assets/maintenance.png"
        }
    ]
}

The same dashboard also contains a CollectionTable filtered with visible_only so operators can inspect the same subset as the map.

4. Add the collection form and delete menu

The table uses the standard collection form flow for create and edit, and a scripts-backed menu for delete. The delete menu calls root.collections.dynamic_map_items.delete, a RUN_SCRIPT action that delegates to delete-collection.js.

root/collections/dynamic_map_items/form/form.web
{
  "moduleId": [
    "modules.web.web-1"
  ],
  "name": "Dynamic map item",
  "description": "Create or update a map-backed collection entry",
  "bindings": {},
  "rights": [],
  "schema": "root.collections.dynamic_map_items",
  "ui": {
    "type": "VerticalLayout",
    "elements": [
      {
        "type": "HorizontalLayout",
        "elements": [
          {
            "type": "Control",
            "scope": "#/properties/name",
            "label": "Name"
          },
          {
            "type": "Control",
            "scope": "#/properties/category",
            "label": "Category"
          },
          {
            "type": "Control",
            "scope": "#/properties/visibleOnMap",
            "label": "Visible on map"
          }
        ]
      },
      {
        "type": "HorizontalLayout",
        "elements": [
          {
            "type": "Control",
            "scope": "#/properties/map/properties/geometry/properties/type",
            "label": "Geometry type"
          },
          {
            "type": "Control",
            "scope": "#/properties/map/properties/geometry/properties/coordinates",
            "label": "Coordinates [lng, lat]"
          }
        ]
      }
    ]
  },
  "initialValue": {
    "visibleOnMap": true,
    "map": {
      "geometry": {
        "type": "Point",
        "coordinates": [
          7.4474,
          46.948
        ]
      }
    }
  },
  "submit": {
    "destination": "Request",
    "type": "Deferred"
  }
}
root/collections/dynamic_map_items/delete/menu/menu.web
{
  "moduleId": [
      "modules.web.web-1"
  ],
  "name": "Dynamic map items menu",
  "description": "Delete actions for map-backed collection entries",
  "menus": [
      {
        "label": "Delete",
        "icon": "delete",
        "context": [
          {
            "type": "CollectionTable",
            "action": "root.collections.dynamic_map_items.delete",
            "condition":"${rows.selected}.length === 0",
            "scopes": ["Click"],
            "input": {
              "scriptId": {
                "expression": "'root.collections.dynamic_map_items.delete'"
              },
              "arguments": {
                "expression": "[{'schema': ${schema}, 'documentId': ${rows.clicked._id}, 'user': ${user.username}}]"
              },
              "validation": {
                "prompt": {
                  "type": "INPUT",
                  "inputType": "confirmation",
                  "labels": {
                      "title": "Confirm",
                      "message": "Confirm deletion",
                      "save": "Delete",
                      "cancel": "Cancel"
                  }
                }
              }
            }
          },
          {
            "type": "CollectionTable",
            "action": "root.collections.dynamic_map_items.delete",
            "condition":"${rows.selected}.length > 0",
            "input": {
              "scriptId": {
                "expression": "'root.collections.dynamic_map_items.delete'"
              },
              "arguments": {
                "expression": "${rows.selected}.map(row => { return {'schema': ${schema}, 'documentId': row._id, 'user': ${user.username}} })"
              },
              "validation": {
                "prompt": {
                  "type": "INPUT",
                  "inputType": "confirmation",
                  "labels": {
                      "title": "Confirm",
                      "message": "Confirm deletion(s)",
                      "save": "Delete",
                      "cancel": "Cancel"
                  }
                }
              }
            }
          }
        ]
      }
  ]
}

5. Runtime behavior

When the frontend sends request-maps-layer for root.layers.collection_standard and root.layers.collection_icons:

  • osp-web opens one backend geojson stream and forwards the current username.

  • osp-maps opens collection stream requests for the configured schema and filter ids.

  • Each matching collection document is transformed into a GeoJSON feature.

  • Further collection updates are pushed automatically to the same websocket response stream.

The table and map both point to the visible_only collection filter, so create, update and delete operations affect the same logical subset without any widget-specific refresh request.