Skip to content

[Proposal] MapStore Widget Model and Communication Sytstem

Lorenzo Natali edited this page Nov 24, 2025 · 26 revisions

Overview

The goal of this improvement is to allow MapStore dashboards and widgets to be extendible and connectable at a higher level than the actual one. The current dependency model relies on fragile, low-level attribute path declarations, limiting bidirectional communication and flexibility. The new system introduces a standardized Event Bus and a Hierarchical Metadata Structure to enable easy, stable, and highly granular coupling of interactive elements (Layers, Traces, Quick Filters).

Proposed By

  • Lorenzo Natali

Assigned to Release

The proposal is for 2026.01.00

State

  • TBD
  • Under Discussion
  • In Progress
  • Completed
  • Rejected
  • Deferred

Motivation

The existing dependency model (dependenciesMap: { "target_attr": "widgets[source_id].source_attr" } ) presents significant limitations:

  • Fragility and Tight Coupling: Dependencies rely on hardcoded paths to widget internal attributes, breaking if the widget's internal state structure changes.
  • Lack of Granularity: It is difficult to target specific internal elements (e.g., a single trace or a specific layer) without exposing complex component internals.No Native Bidirectional Flow: Handling loops and defining two-way communication (e.g., Map selects $\leftrightarrow$ Table filters) is complicated and error-prone.
  • Poor UI/UX: The configuration UI cannot intuitively display connection options because widget roles are not standardized or public.

This proposal aims to replace the Pull Model (Target pulls data) with a robust, pluggable event push model. Interactivity, that now is implemented only between widgets and map (only in one way), now should be in 2-ways, and support to interact with other tools/data.

Current interactions

  1. Map widget can:
    1. filter by extent other widget (table, chart, counter). This can be done only if target as a geom
      1. table - single dependency
      2. chart - multiple charts, multiple layers --> this actually happens on every chart.
      3. single dependency.
    2. pass layers/groups information to other widgets (legend)
    3. control center and zoom of another map
  2. Table widget can:
    1. apply filter on columns to other widget (quickFilters)
    2. apply a filter and so filter the same layer by name on map
    3. apply a filter to a trace of a chart of a chart widget
    4. zoom to extent using filter on columns bounds in map widget (quickFilters)
    5. zoom to a single feature in map widget
  3. Legend widget can:
    1. update visibility, expanded and opacity properties of layers in map widget
    2. apply interactive legend filter when available
  4. dependency propagation
  5. mapSync flag
  6. viewparams management.
  7. Global Map as source
  8. Global Map layers as target
  9. timeline as source can add current time/Range.

The new architecture leverages the existing Redux Store as the event bus. The core innovation is the introduction of a central Wiring Map managed by a Redux Middleware, which decouples the events from the targets.

Core Architectural Components

  1. Event Bus (Redux): The central channel for standardized events (INTERACTION_EVENT).
  2. Wiring Map: A flat list of directional arcs (Source $\to$ Target) defining active connections.
  3. Routing Middleware: The logic component that intercepts INTERACTION_EVENT and uses the Wiring Map to dispatch targeted WIDGET_UPDATE actions (or other, like LAYER_UPDATE... depending on the target).
graph LR
    %% Widgets and Layers
    W1["Table widget T (Emitter)"]
    W2["Map widget M (Receiver)"]
    L1["Map Layer L (Receiver, part of Map widget)"]
    F1["Chart trace D (Receiver)"]

    %% Event Bus
    EB["Event Bus (forwards interaction events to all the listeners triggering the proper action)"]

    %% Event flow
    W1 -->|INTERACTION_EVENT| EB
    EB -->|ZOOM_TO_FEATURES| W2
    EB -->|FILTER_LAYER| L1
    EB -->|FILTER_LAYER| F1
Loading

Proposal

Note:

Some special cases found during this investigation still need to be evaluated.

  • evaluate the viewparams use case.
  • evaluate the timeline shapes use case: in case of date axis, we may receive current time from timeline, as a shape. We can evaluate to keep it as it is now or to map, maybe in the future, as an improvement.

Design

The design is centered on the following processes:

  1. Configuration
  2. Metadata
  3. List of widgets with relative events and types
  4. Backward compatibility

The general architecture is dual:

  • A graph represents the ineractions between the elements.
  • A metadata tree, that represents our interactive elements, and define the rules of possible connections

1. Configuration

1.1 Graph of interactions

The first enhance that we need to do in order to make the system more flexible and manageble is to transform the logic from pull into push. In order to properly manage the communication between a set of nodes, we need to create a graph structure rather then a tree structure. This allows items to be connected with a matrix straegy, and use graph theory algorithm to manage the items and the connections.

So, near to the widgets, we will persist a new interactions entry, that represents the archs.

This interactions is an array of entries like the following:

{
  "id": "interaction-1234",
  "source": {
    "nodePath": "widgets['86009090-53c8-11ea-bc50-9931c4e30b1f'].maps['map-1-id']", // navigable path from root tree
    "eventType": "eventType"   // matches interactionMetadata.eventType
  },
  "target": {
    "nodePath": "widgets['6df30870-53c8-11ea-bc50-9931c4e30b1f'].maps['ee158970-1a53-11ed-9687-73e2d620f2c0'],
    "target": "viewport",           // semantic property (from interactionMetadata.targets.targetProperty)
    "mode": "update"
  },
  "transform": [],
  "enabled": true
}

Properties

  • nodePath Fully navigable path inside the generated metadata tree. Same logic as JavaScript bracket notation but evaluated by a custom resolver, not by JS itself.
  • eventType Must match one entry in interactionMetadata.events.eventType in metadata.
  • target Must match one interactionMetadata.targets.targetProperty.
  • modeInterpreted by target adapter:
    • "upsert": Only if there is a n array as target they are replaced if already exist (identify by id). update or append depending on data type, the item is identified by an ID to identify if the object is present or not. Typically filters inside the filters of a MapStore filter can be done in this way. So in this case we need to replace an element with the same id, if exists, or to insert
    • "replace": replace whole object.
  • transform Array of ordered transformations. Each transform receives { data, source, target } and modifies the data type or do some operation on it (e.g. a intermediate request, like in the case of 'zoom to features' that should invoke the WPS to calculate bbox).

Note

For upsert, also delete of the widget and so interaction should be considered. On widget removal interactions need to be removed, and some data may need to e removed (e.g. filter applied in upsert mode). This should be considered during development. A possible solution can be to use an object like dependencies that is generated on the fly. But this can apply only on widgets, not on layer. So clean-up policy for map will need anyway to be done. This makes evident that this is the policy that should be used also on widgets.

1.2 Example implementation

When an interaction is registered to at least one target, the application will emit on the eventevent an INTERACTION_EVENT action on redux. Like this:

{
  "type": "INTERACTION_EVENT",
  "payload": {
    "traceId": "1234-asddfgf" // uniquely generated trace-id on event trigger. This can be used if we need to prevent loops.
    "eventType": "viewportChange",
    "dataType": "BBOX_COORDINATES",
    "data": { "minx": 1, "miny": 2, "maxx": 3, "maxy": 4, "crs": "EPSG:3857" }, // effective payload
    "sourceNodePath": "widgets['w-id'].maps['map-1-id']",
    "extra": { "zoom": 12, "scale": 5000, "originTraceId": "uuid-v4" } // extra information, if needed
  }
}

The event should can be triggered by the widget itself and can be triggered by:

  • changing a property (e.g. viewport change, filter change)
  • user inserts something (click on zoom to Viewport)

This will translate into propert "WIDGET_UPDATE" ( OR CHANGE_LAYER_PROPERTIES or whatever) by a dedicated epic that:

  • Monitors the interactions array, collecting and listening for the events.
  • On each INTERACTION_EVENT, find the interactions that involve the event triggered (By source path and type). For each interaction found dispatches the proper actions.
    • on the widgets --> and so change widget proprties.
    • on the map --> and so trigger the proper events on layers

1.3 Canonical lists

1.3.1 datatypes

Here a list of examples of possible datatypes to be triggered. During development we will define them definitevely.

  • BBOX_COORDINATES → {minx,miny,maxx,maxy,crs}
  • LAYER_FILTER → { filter: <MAPSTORE_FILTER>, layer: {name, url }
  • POINT → { x,y,crs }
  • NUMBER → { value: number }
  • STRING → { value: string }
  • STRING_ARRAY → { values: string[] }
  • OBJECT_ARRAY: { values: [{prop1: value1, prop2: value2 ] } // ths have to be defined well, probably for multi-selection in dyn filter.
  • FEATURE → { properties, geometry }
  • FEATURE_ARRAY → { features: [] }
  • (future) DATE, DATETIME
1.3.2 events

Here a list of examples of possible events to be triggered. During development we will define them definitevely.

  • Map: viewportChange (BBOX_COORDINATES), centerChange (POINT), zoomChange (NUMBER), featureClick (FEATURE)
  • (future) Chart: traceClick (FEATURE or properties), layerFilterChange (LAYER_FILTER)
  • Table: layerFilterChange (LAYER_FILTER), zoomClick (FEATURE)
  • Legend: visibilityToggle (LAYER_FILTER or layerId + visible boolean) <-- to evaluate how to implement connection with legend
  • DynamicFilter: (sub-section) selectionChange (FEATURE_ARRAY, FEATURE, STRING_ARRAY, STRING), depending on the type. >> TODO: decide if we can rely always on ARRAY in any case (e.g. using transformations in case of needing or what).

1.3.4 targetType examples

  • zoomToViewport (expects BBOX_COORDINATES, mode update) >> IN CASE OF FILTER on table that generates a zoom to feature, we have transformation.

  • viewportFilter (extect BBOX_COORDINATES)

  • applyFilter (expects LAYER_FILTER, mode upsert, with LAYER corrispondence)

  • changeCenter (POINT)

  • changeZoom (NUMBER)

2. Metadata

2.1 Component Structure and Hierarchy

The interaction points are organized in a navigable tree to support complex nesting (e.g., Charts inside a Widget, Traces inside a Chart). Non-interactive structural nodes (collection) are used for grouping. This tree structure can be generated by a function that receives the current widgets configuration (and information about other plugin that can interact, e.g. map plugin).

This tree, properly filtered, can be used to propose the UI to the user for selecting the proper targets for events.

flowchart TD

    %% ROOT
    ROOT["root (element)"]

    %% MAP CASE
    ROOT --> MAP["map (element)"]
    MAP --> MAP_LAYERS["layers (collection)"]

    MAP_LAYERS --> L1["layer-1 (element)"]
    MAP_LAYERS --> L2["layer-2 (element)"]

    %% WIDGETS COLLECTION
    ROOT --> WCOLL["widgets (collection)"]

    %% WIDGET EXAMPLE
    WCOLL --> W1["widgets['w-id-1'] (element) chart widget"]
    WCOLL --> W2["widgets['w-id-2'] (element) map widget"]
    WCOLL --> W3["widgets['w-id-3'] (element) table widget"]
    WCOLL --> W4["widgets['w-id-4'] (element) counter widget"]
    WCOLL --> W5["widgets['w-id-5'] (element) legend widget"]

    %% CHARTS INSIDE WIDGET
    W1 --> CHCOLL["charts (collection)"]
    CHCOLL --> CH1["chart[id=chart-id-1] (element)"]

    %% TRACES INSIDE CHART
    CH1 --> TRCOLL["traces (collection)"]
    TRCOLL --> TR1["trace[id=trace-id-1] (element)"]

    %% MAPS INSIDE WIDGET
    W2 --> WMAPCOLL["maps (collection)"]
    WMAPCOLL --> WMAP1["map[id=map-1-id] (element)"]

    %% MAP LAYERS INSIDE WIDGET MAP
    WMAP1 --> WLAYER_COLL["layers (collection)"]
    WLAYER_COLL --> WLAYER1["layer[id=layer-id-1] (element)"]

Loading

Note

this stucture of metadata is not saved anywere, but generated on the fly, from a node, to easely:

  • find events emitted by a widget/tool.
  • generate UI for wiring selection
  • filter by expectedDataType possible targets (and exclude invalid targets).

2.2 General tree structure and definitions

Every node of the tree can be of 2 types:

  • element: is the effective interactive unit. IT can be identified by name or id. Represent typically an object.
  • collection: is a group of nodes. Have children only with id. Represents typically an array.

The path to each node is identified by name or id. They can not be both present.

With this abstraction we want to represent:

  • widgets is a collection, containing all the widgets identifieable by id.
  • widgets['w-id-1'] is an element
  • widgets['w-id-1'].charts is a collection.
  • widgets['w-id-1'].charts['chart-id-1'] is an element
  • widgets['w-id-1'].charts['chart-id-1'].traces is a collection
  • widgets['w-id-1'].charts['chart-id-1'].traces['trace-id'] is an element
  • widgets['w-id-1'].maps is a collaction
  • widgets['w-id-1'].maps['map-id-1'] is an element
  • widgets['w-id-1'].maps['map-id-1'].layers is a collection
  • map is an element (only in case of widgets on map).
  • map.layers: is a collection, containing several elements: map.layers['layer-1'], `map.layers['layer-2'].

2.3 node elements

  • id: is the identifier of an element in a collection
  • name is the identifier of an element or a collection in an element
  • title: title to describe the element ( to be localized ).
  • icon: optional icon to show in UI. can have the prefix ms- for mapstore icon, fa- for font-awsome icons.
  • children: an array of element that belong to the element.
  • interactionMetadata only element can have them. Define the contract in terms of actions and targets.
    • events: an array of entries that have:
      • eventType an event that triggers the event.
      • dataType the data type emitted.
      • eventProperties: some specific event properties, to be used as additional utility for data matching (e.g. layer, attributeName ...). These can be decided at development time due to effective development needings.
    • targets: an array of have that have:
      • targetType: Represent the effect we have (zoom to feature)
      • expectedDataType: The dataType expected.
      • targetProperty the attribute where to "write" the received data.
      • mode (update, upsert).
      • default What happens in initial condition (TBD)

This model allows, given the widget i'm implementing, to

  • easely identify the events emitted (by dataType) of a widget. In case of new widget we suggest to have a function that extract this data recursively from a generic node.
  • The events that can be received (matching with expectedDataType). Properly filtering the tree, we can create the tree of possible targets in the UI.

2.4 Examples:

map widget:

{
    "type": "element",
    "title": "Map 1",
    "id": "id-map-widget-1",
    "children": [{
       "type": "collection",
       "name": "maps",
       "children": [{
          "id": "map-1-id",
          "type": "element",
          "interactionMetadata": {
            "actions": [
              { "eventType": "viewportChange", "dataType": "BBOX_COORDINATES" }
            ],
            "targets": [
              { 
                "targetType": "zoomToBBox",
                "targetProperty": "viewport",
                "expectedDataType": "BBOX_COORDINATES",
                "mode": "update" 
              }
            ]
          },
         "children": [{
            "type": "collection",
            "name": "layers",
            "children": [{
               "id": "layer-id-1", 
               "interactionMetadata": {
                    "events": [
                    ],
                    "targets": [
                      { 
                        "targetType": "filter",
                        "targetProperty": "filter",
                        "expectedDataType": "MAPSTORE_FILTER",
                        "mode": "upsert",
                        "targetKey": "layerFilter.filters" 
                       }
                    ]
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

2.5 Root node

The root node can be used as an initial container for valid communication targets. It has the advantages to:

  • allow to implement both the cases that we have only widgets (dashboard), where we have also the standard map (map with witdgets) and future expansions (e.g. timeline, feature info...).
  • It's easy to generate the path using this tree, with a recursive algorithm, removing root. from the beginning of the path.

Here in the case of dashboard

{
   "type": "element",
   "name": "root",
   "children": [{
        "type": "collection",
        "name": "widgets",
        "children": [] 
    }]
}

Here the root in case of map

{
   "type": "element",
   "name": "root",
   "children": [{ 
        "type": "element",
        "name": "map",
        "interactionMetadata": {
            "events": [
              { "eventType": "viewportChange", "dataType": "BBOX_COORDINATES"},
              { "eventType":"centerChange", "dataType": "POINT"},
              { "eventType":"zoomChange", "dataType": "NUMBER"},
            ],
            "targets": [
              { 
                "targetType": "zoomToViewport",
                "attributeName": "viewport",
                "expectedDataType": "BBOX_COORDINATES",
                "mode": "update" 
              },
              { 
                "attributeName": "center",
                "expectedDataType": "POINT",
                "mode": "update" 
              },
              { 
                "attributeName": "zoom",
                "expectedDataType": "MU",
                "mode": "update" 
              }
            ]
          },
        "children": [{
          "type": "collection",
          "name": "layers",
          "children": []
        }]
   }, {
        "type": "collection",
        "name": "widgets",
        "children": [] 
    }]
}

2.6 Metadata tree generator

There should be a interactionMetadataTreeSelector that takes the application state and generates the metadata tree. Because this structure can be very big, it should be properly memorized using the selectors. Moreover, in order to avoid to have a unique big function that hardcodes the behaviors, it should be break in pieces properly tested and reusable.

The ideal is to make it pluggable, but for the moment lets be limited to having it defined statically.

E.g.

// utility functions
function generateWidgetMetadataTree(widget, widgetRegistry) {
    if(widgetRegistry?.[widget.type]?.interactionMetadataTreeGenerator) {
         return widgetRegistry?.[widget.type]?.interactionMetadataTreeGenerator(widget);
    }
    }
    // defaults?
}

function generateWidgetsMetataTree(widgets) {
    const collection = { 
       type: 'collection', 
       name: 'widgets', 
       children: widgets.map(w => generateWidgetMetadataTree() };
    ;
}
// main selector (evaluate to use `redux-select` to cache, or `react` caching system)
function generateInteractionMetadataTree(state, {pluginsRegistry, widgetRegistry} ) { // widgetRegistry can contain specific implementations of tree generation by type.
   const root = { type: 'element', name: 'root', children: [] };
   let map = undefined;
   let widgets = undefined;
   if (state.map && hasMapPlugin(plugins)) {
         map = generateMapMetadataTree(state.map, pluginsRegistry);
         // add map to root
    }; 
    if (hasWidgets && hasWidgetsPlugin(plugins) {
         widgets = generateWidgetsMetataTree(state.widgets, widgetsRegistry); 
         // add widgets too root
   }
     

2.7 Events and targets by widget

We need to create a function that produces for each element (Widget or sub-part of it) a tree, recursively. This allows to iterate widgets and produce the tree, that can be filtered to create the UI of MapStore for editing.

2.7.1 Map

The principal map. emit events:

  • emit viewport change
  • emit center changes and zoom changes
  • (future) emit clicks on features (so features, clicked coordinates). receive:
  • receive viewport, to zoom to the viewport
  • receive a layers_config_change (as visibility, opacity etc...) - this is managed on map level
  • receive a zoom to feature event
  • receive a zoom to bbox of a given filter (like now quickfilters work to bind map). In order to implement this, we will need to call WPS as it is done now by current binding. This can be implemented via transformation.

Moreover the map can have several layers. Each layer has an ID. We may decide to show them in groups via UI, or show groups as targets too in the future, but for now is ok to represent layers as a sub-collection of the Map.

Layers can :

  • receive filters from the feature grid (quick filters). In this case, we can make match the same layer.
  • receive filters by dynamic Filter (e.g. a category selected). This needs to be configured with the CQL_FILTER template. e.g. attribute_name = ${feature.properties.attribute_to_filter}
  • (future) receive filters by viewport or by selected feature(s) (e.g. intersect)

Finally, the legend or other tools can update layers at all, without to need to collect single layers.

Here a draft on how the tree can be implemented (TBC).

{ 
    "type": "element",
    "name": "map",
    "interactionMetadata": {
        "events": [
            { "eventType": "viewportChange", "dataType": "BBOX_COORDINATES"}, // triggered when map viewport changes
            { "eventType":"centerChange", "dataType": "GEOMETRY_POINT"}, // triggered when pan
            { "eventType":"zoomChange", "dataType": "NUMBER" } // Triggered when zoom
        ],
        "targets": [
            { 
                "targetType": "zoomToViewport",
                "attributeName": "viewport",
                "expectedDataType": "BBOX_COORDINATES",
                "mode": "update"
            },
            { 
                "targetType": "changeCenter",
                "attributeName": "center",
                "expectedDataType": "POINT",
                "mode": "update" 
            },
            { 
                "targetType": "changeZoom",
                "attributeName": "zoom",
                "expectedDataType": "NUMBER",
                "mode": "update" 
            }, {
                
        ]
        },
        "children": [{
            "type": "collection",
            "name": "layers",
            "children": [{
                "type": "element",
                "id": "layer-bcsdf12345",
                "title": "Sample Layer",
                "interactionMetadata": {
                    "events": [
                    ],
                    "targets": [
                        {
                            "targetType": "applyFilter",
                            "expectedDataType": "LAYER_FILTER",
                            "constraints": {
                                "layer": {"name": "topp:states", "url": "https://demo.mapstore.org/geoserver/wfs"} 
                                
                            },
                            "mode": "upsert"
                        },
                        {
                            "targetType": "filterByViewport",
                            "expectedDataType": "BBOX_COORDINATES",
                            "mode": "upsert"
                        }
                    ]
                }
        }]
    }]
}

2.7.2 Map Widget

Map Widget contains many maps, so basically the shape is the same.

 { 
    "type": "collection",
    "name": "widgets",
    "children": [{
      "type": "element",
      "id": "map-widget-1234-5676",
      "children": [{
        "type": "collection",
        "name": "maps"
        "title": "Maps",
        "children": [{ // HERE THE SAME MAP OF THE MAIN MAP EXAMPLE
            "type": "element",
            "id": "map-1234-6789rgfg£-45fdgh5",
            "interactionMetadata": {...},
            "children": [...] // here a collection of layers
        }]
    }]
}

2.7.3 Legend widget

Legend widget is a special case. It NEEDS to have to be connected to a widget and the widget will connect directly to the state of this widget that will be configured on legend widget creation.

  • when connected always receives also information from map ( resolution/scale, bbox, layers...)
  • when the user clicks on a checkbox or something, the map layer's visibility have to update.

So this 2-way binding have to be managed. We can maintain the actual system or define an additinal strategy inside the system to manage it. As an option, to define, we can do for legend widget a custom UI that anyway defines several interactions between the components, ovoiding the UI

chart widget

Contains multiple charts. Now only chart traces can receive,

So the chart will be something like

{
      "type": "element",
      "id": "widget",
      "children": [{
        "type": "collection",
        "name": "charts",
        "title": "Chart 1",
        "children": [{
            "type": "collection",
            "name": "traces",
            "interactionMetadata": {},
            "children": [{
                "type": "element",
                "title": "peolple states",
                "icon": "bar-chart",
                "id": "trace1-sdfsdf-sdfsd-fdsf",
                "interactionMetadata": {
                    "events": [
                    ],
                    "targets": [
                        {
                            "targetType": "applyFilter",
                            "expectedDataType": "LAYER_FILTER",
                            "targetProperty": "dependencies.filters",
                            "constraints": {
                                "layer": { "name": "topp:states" }
                            },
                            "mode": "upsert"
                        },
                        {
                            "targetType": "filterByViewport",
                            "targetProperty": "dependencies.viewports",
                            "expectedDataType": "BBOX_COORDINATES",
                            "mode": "upsert"
                        }
                    ]
                }
            }] 
        }]
    }]
}

2.7.4 Table widget

The table widget has actually only one table. It can trigger the events of filter change and zoomToFeature. old quickfilters will be transformed into a filter change.

{
      "type": "element",
      "id": "widget-123-4556",
      "title": "My table", 
      "icon": "table",
      "interactionMetadata": {
        "events": [
            { 
                "eventType": "filterChange", 
                "dataType": "LAYER_FILTER", 
                "layer": {"name": "topp:states", "url": "https://demo.geo-solutions.it/geoserver/wfs"} 
            },
            { 
                "eventType": "zoomToFeature", 
                "dataType": "FEATURE",
                "layer": {"name": "topp:states", "url": "https://demo.geo-solutions.it/geoserver/wfs"} 
            }
        ],
        "targets": [
            {
                "targetType": "applyFilter",
                "expectedDataType": "LAYER_FILTER",
                "targetProperty": "dependencies.filters",
                "constraints": {
                    "layer": {"name": "topp:states", "url": "https://demo.geo-solutions.it/geoserver/wfs"} 
                },
                "mode": "upsert"
            },
            {
                "targetType": "filterByViewport",
                "targetProperty": "dependencies.viewports",
                "expectedDataType": "BBOX_COORDINATES",
                "mode": "upsert"
            }
        ]
    }
}

Note

In this case we can notice several particular things that have to be managed.

  1. The layer must match to apply events. This means that the UI should propose only the target filters that use the same layer (or that have a proper transformation).
  2. The tool emits and receives filters. Of course in the UI we should deny to apply events when source and target path are the same.
  3. The tool has an additional filter as viewport. The emitted filter must contain also the viewport filter.
  4. The filter is in mode upsert. This means that the source data must have an ID that allows to properly update or insert, depending on the presence of the old filter.
  5. The interaction that filter the layer on the filter change can be applied to the target layer of a map. Anyway the zoom is an additional interaction that needs a transformation.

2.7.5 Dynamic filter

This implementation will be developed to provide the user the possibility to use a dynamicFilter widget that allows the user to do some selection and apply the effects.

The dynamic filter will emit

  • single values (string, number)
  • multiple values (string[], number[]).

Is always necessary to apply a transformation in this case. From the UI, depending on the target element (e.g. a layer), we can apply a filter to the layer and the value will need to be used as a Furure changes may allow to toggle some layer visibility or something else.

2.8 Transformations

A transformation can be a list of possible conversions.

Example definition:

{
   name: 'layer-filter-to-bbox',
   inputDataType: 'LAYER_FILTER',
   outputDataType: 'BBOX_COORDINATES',
   parameters: [{ name: 'geometryAttributeName', type: "string"}]
}

-'layer-filter-to-bbox': Actually we can trasform layer into a bbox, by invoking the WPS.

3 Building the UI

Given the layer of interaction metadata, for a given widget or tool, we can derive the list of events emitted. From the same tree, we can find:

  • the targets that can receive the event because of the same data type.
  • the targets that can receive the evente because of the same data type after an available transformation. In the case we need a UI for the proper transformation.

Typically the interactions UI is a tree of type

Here an example for the table widget on topp:states.

> When filter changes
   > 🗺️ Map Widget
     > Map 1 
      > Zoom to features      '🔌'
      > Layers 
        > "States"  
           > Filter Layer     '🔌'
> When click on zoom 
   > 🗺️ Map Widget
     > Map 1 
      > Zoom to feature       '🔌'

In case it need a con figuration '⚙️' should be added. Clicking on it, you can insert in the node the configuration to add to the binding.

E.g. on dynamic filter

> Tool 1: On selection
   > 🗺️ Map Widget
     > Map 1 
      > Zoom to features     '⚙️' '🔌'
      > Layers 
        > "States"  
           > Filter Layer         '🔌'

In the case of main map as source of event, we need to design the proper interface to bind this particular behavior. Maybe it is possible to make it configurable:

  • From target widget (to make the configuration easier and less convolute)
  • From map settings (as a possible alternative).

This have to be decided at app design time.

4. Backward comaptibility

In order to maintain guarantee a better migration, on dashboard/map load the settings will be transformed in the new system.

A notification will be sent to the user (if able to edit the current map/dashboard) that tells the user that the current wiring has been converted and to re-save the current resource to persist the new version. A link to migration guidelines in the notification will explain the user what are the parts that can not be migrated automatically and how to manually work on them.

In case of errors in conversion, a notification tells the user that an unexpected error happened during the widget migration, therefore all connections has been removed.

Because probably there will be additional enhancements to this system, it is useful to provide a version to save with together with interactions, widgets etc... in the resource, to properly manage future migration (by incremental apply of migration function).

Automatic migration

In order to automatically migrate the dependencies here I report some examples of current wiring system.

	   // map 
	   dependenciesMap": {
        "filter": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].filter",
        "quickFilters": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].quickFilters",
        "layer": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].layer",
        "options": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].options",
        "dependenciesMap": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].dependenciesMap",
        "mapSync": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].mapSync"
      },
      // chart
	   "dependenciesMap": {
        "viewport": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].maps[ee158970-1a53-11ed-9687-73e2d620f2c0].viewport",
        "layers": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].maps[ee158970-1a53-11ed-9687-73e2d620f2c0].layers",
        "filter": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].filter",
        "quickFilters": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].quickFilters",
        "layer": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].layer",
        "options": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].options",
        "mapSync": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].mapSync",
        "dependenciesMap": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].dependenciesMap"
      },
      
      // legend
      "dependenciesMap": {
        "layers": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].maps[ee158970-1a53-11ed-9687-73e2d620f2c0].layers",
        "zoom": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].maps[ee158970-1a53-11ed-9687-73e2d620f2c0].zoom",
        "viewport": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].maps[ee158970-1a53-11ed-9687-73e2d620f2c0].viewport",
        "dependenciesMap": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].dependenciesMap",
        "mapSync": "widgets[6df30870-53c8-11ea-bc50-9931c4e30b1f].mapSync"
      },
      // counter 
      "dependenciesMap": {
        "filter": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].filter",
        "quickFilters": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].quickFilters",
        "layer": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].layer",
        "options": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].options",
        "dependenciesMap": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].dependenciesMap",
        "mapSync": "widgets[86009090-53c8-11ea-bc50-9931c4e30b1f].mapSync"
      },

dependenciesMap defines wich attributes a widget receives.The dependencies were spreaded automatically identifying the same layer property. With the new wiring definition, the wiring must be spreaded explicitly following the same approach.

In order to widely test the features, we have to preventively export old dashboards significative use cases to create valid unit tests.

Clone this wiki locally